From 40db6ad6777341d732a8a4eb0b9cd43cd8c406dd Mon Sep 17 00:00:00 2001 From: "xiayu.lyt" Date: Wed, 10 Sep 2025 14:13:06 +0800 Subject: [PATCH] add unit-test Signed-off-by: xiayu.lyt --- pkg/config/cloud_config.go | 8 +- pkg/config/cloud_config_test.go | 189 + pkg/config/controller_config.go | 2 +- pkg/config/controller_config_test.go | 258 ++ pkg/config/feature_gates.go | 2 +- pkg/config/feature_gates_test.go | 93 + pkg/config/runtime_config_test.go | 12 + pkg/context/base/context_test.go | 199 + pkg/context/shared/sharedcontext_test.go | 143 + pkg/controller/controller_test.go | 54 + pkg/controller/helper/endpoint_utils_test.go | 149 + pkg/controller/helper/event_test.go | 47 + pkg/controller/helper/finalizer_test.go | 198 + pkg/controller/helper/net_utils_test.go | 99 + pkg/controller/helper/node_utils.go | 10 +- pkg/controller/helper/node_utils_test.go | 771 +++- pkg/controller/helper/pod_utils_test.go | 361 ++ pkg/controller/helper/queue_test.go | 520 +++ .../helper/reference_indexer_test.go | 182 + pkg/controller/helper/service_utils_test.go | 490 +- pkg/controller/helper/tags_test.go | 96 + pkg/controller/node/event_handler_test.go | 167 + pkg/controller/node/manager.go | 4 +- pkg/controller/node/manager_test.go | 532 +++ pkg/controller/node/node_controller.go | 84 +- pkg/controller/node/node_controller_test.go | 917 +++- pkg/controller/route/event_handler_test.go | 278 ++ pkg/controller/route/manager_test.go | 526 ++- pkg/controller/route/predicate.go | 2 +- pkg/controller/route/predicate_test.go | 207 + pkg/controller/route/route_controller.go | 48 +- pkg/controller/route/route_controller_test.go | 598 ++- pkg/controller/service/clbv1/coverage | 1 + .../service/clbv1/event_handler_test.go | 256 +- .../service/clbv1/listeners_test.go | 751 +++- .../service/clbv1/loadbalancer_test.go | 465 ++ .../service/clbv1/model_applier_test.go | 304 ++ .../service/clbv1/service_controller_test.go | 317 +- pkg/controller/service/clbv1/vgroups_test.go | 1172 ++++- pkg/controller/service/nlbv2/coverage | 1 + pkg/controller/service/nlbv2/coverage-nlbv2 | 1204 +++++ .../service/nlbv2/coverage-server-groups | 1204 +++++ .../service/nlbv2/event_handler_test.go | 154 +- .../service/nlbv2/listeners_test.go | 342 +- .../service/nlbv2/loadbalancer_test.go | 534 +++ .../service/nlbv2/model_applier_test.go | 155 +- .../service/nlbv2/nlb_controller_test.go | 375 ++ .../service/nlbv2/server_groups_test.go | 3987 +++++++++++++++++ .../reconcile/annotation/annotations_test.go | 224 +- .../service/reconcile/backend/backend_test.go | 830 ++++ pkg/model/load_balancer.go | 5 +- pkg/model/load_balancer_test.go | 328 ++ pkg/model/nlb/nlb.go | 2 +- pkg/model/nlb/nlb_test.go | 413 ++ pkg/provider/vmock/ecs.go | 26 +- pkg/provider/vmock/nlb.go | 36 +- pkg/provider/vmock/slb.go | 21 +- pkg/provider/vmock/vpc.go | 106 +- pkg/util/attempt_test.go | 138 + pkg/util/crd/crd_test.go | 178 + pkg/util/hash/hash_test.go | 49 +- pkg/util/metric/metrics_test.go | 114 + pkg/util/reconcile_test.go | 53 + pkg/util/utils.go | 3 +- pkg/util/utils_test.go | 207 + .../clientset/fake/clientset_generated.go | 92 + .../client/clientset/clientset/fake/doc.go | 20 + .../clientset/clientset/fake/register.go | 58 + .../typed/apiextensions/v1/fake/doc.go | 20 + .../v1/fake/fake_apiextensions_client.go | 40 + .../v1/fake/fake_customresourcedefinition.go | 178 + .../typed/apiextensions/v1beta1/fake/doc.go | 20 + .../v1beta1/fake/fake_apiextensions_client.go | 40 + .../fake/fake_customresourcedefinition.go | 178 + .../client-go/discovery/fake/discovery.go | 174 + vendor/modules.txt | 4 + 76 files changed, 21710 insertions(+), 315 deletions(-) create mode 100644 pkg/config/cloud_config_test.go create mode 100644 pkg/config/controller_config_test.go create mode 100644 pkg/config/feature_gates_test.go create mode 100644 pkg/config/runtime_config_test.go create mode 100644 pkg/context/base/context_test.go create mode 100644 pkg/context/shared/sharedcontext_test.go create mode 100644 pkg/controller/controller_test.go create mode 100644 pkg/controller/helper/endpoint_utils_test.go create mode 100644 pkg/controller/helper/event_test.go create mode 100644 pkg/controller/helper/finalizer_test.go create mode 100644 pkg/controller/helper/net_utils_test.go create mode 100644 pkg/controller/helper/pod_utils_test.go create mode 100644 pkg/controller/helper/queue_test.go create mode 100644 pkg/controller/helper/reference_indexer_test.go create mode 100644 pkg/controller/node/event_handler_test.go create mode 100644 pkg/controller/route/event_handler_test.go create mode 100644 pkg/controller/route/predicate_test.go create mode 100644 pkg/controller/service/clbv1/coverage create mode 100644 pkg/controller/service/nlbv2/coverage create mode 100644 pkg/controller/service/nlbv2/coverage-nlbv2 create mode 100644 pkg/controller/service/nlbv2/coverage-server-groups create mode 100644 pkg/model/nlb/nlb_test.go create mode 100644 pkg/util/attempt_test.go create mode 100644 pkg/util/crd/crd_test.go create mode 100644 pkg/util/metric/metrics_test.go create mode 100644 pkg/util/reconcile_test.go create mode 100644 pkg/util/utils_test.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go create mode 100644 vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go create mode 100644 vendor/k8s.io/client-go/discovery/fake/discovery.go diff --git a/pkg/config/cloud_config.go b/pkg/config/cloud_config.go index e9b530886..0c30b9ae6 100644 --- a/pkg/config/cloud_config.go +++ b/pkg/config/cloud_config.go @@ -61,11 +61,11 @@ func (cc *CloudConfig) LoadCloudCFG() error { if err != nil { return fmt.Errorf("read cloud config error: %s ", err.Error()) } - err = yaml.Unmarshal(content, CloudCFG) + err = yaml.Unmarshal(content, cc) if err != nil { return err } - CloudCFG.SetDefaultValue() + cc.SetDefaultValue() return nil } @@ -79,8 +79,8 @@ func (cc *CloudConfig) SetDefaultValue() { if cc.Global.RouteMaxConcurrentReconciles == 0 { cc.Global.RouteMaxConcurrentReconciles = DefaultRouteMaxConcurrentReconciles } - CloudCFG.Global.ResourceGroupID = strings.TrimSpace(CloudCFG.Global.ResourceGroupID) - CloudCFG.Global.RouteTableIDS = strings.TrimSpace(CloudCFG.Global.RouteTableIDS) + cc.Global.ResourceGroupID = strings.TrimSpace(cc.Global.ResourceGroupID) + cc.Global.RouteTableIDS = strings.TrimSpace(cc.Global.RouteTableIDS) } func (cc *CloudConfig) GetKubernetesClusterTag() string { diff --git a/pkg/config/cloud_config_test.go b/pkg/config/cloud_config_test.go new file mode 100644 index 000000000..27f3269b2 --- /dev/null +++ b/pkg/config/cloud_config_test.go @@ -0,0 +1,189 @@ +package config + +import ( + "k8s.io/cloud-provider-alibaba-cloud/pkg/util" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + testCloudConfig = ` +global: + accessKeyId: dGVzdC1hY2Nlc3Mta2V5 + accessKeySecret: dGVzdC1rZXktc2VjcmV0 + clusterID: test-cluster-id + resourceGroupID: rg-123456 + region: cn-hangzhou + routeTableIDS: vtb-123456 + serviceBackendType: ecs + uid: 123456 + vpcID: vpc-123456 + vswitchID: cn-hangzhou-a:vsw-123456,cn-wulanchabu-c:vsw-234567 + serviceMaxConcurrentReconciles: 5 + nodeMaxConcurrentReconciles: 2 +` +) + +func TestCloudConfig_SetDefaultValue(t *testing.T) { + cfg := &CloudConfig{} + cfg.SetDefaultValue() + + assert.Equal(t, DefaultServiceMaxConcurrentReconciles, cfg.Global.ServiceMaxConcurrentReconciles) + assert.Equal(t, DefaultNodeMaxConcurrentReconciles, cfg.Global.NodeMaxConcurrentReconciles) + assert.Equal(t, DefaultRouteMaxConcurrentReconciles, cfg.Global.RouteMaxConcurrentReconciles) + + cfg = &CloudConfig{} + cfg.Global.ServiceMaxConcurrentReconciles = 10 + cfg.Global.NodeMaxConcurrentReconciles = 15 + cfg.Global.RouteMaxConcurrentReconciles = 20 + cfg.SetDefaultValue() + + assert.Equal(t, 10, cfg.Global.ServiceMaxConcurrentReconciles) + assert.Equal(t, 15, cfg.Global.NodeMaxConcurrentReconciles) + assert.Equal(t, 20, cfg.Global.RouteMaxConcurrentReconciles) +} + +func TestCloudConfig_GetKubernetesClusterTag(t *testing.T) { + cfg := &CloudConfig{} + tag := cfg.GetKubernetesClusterTag() + assert.Equal(t, util.ClusterTagKey, tag) // util.ClusterTagKey 的值 + + cfg = &CloudConfig{} + customTag := "my-custom-cluster-tag" + cfg.Global.KubernetesClusterTag = customTag + + tag = cfg.GetKubernetesClusterTag() + assert.Equal(t, customTag, tag) +} + +func TestCloudConfig_LoadCloudCFG_YAML(t *testing.T) { + tmpfile, err := os.CreateTemp("", "cloud-config") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + _, err = tmpfile.Write([]byte(testCloudConfig)) + assert.NoError(t, err) + err = tmpfile.Close() + assert.NoError(t, err) + + oldPath := ControllerCFG.CloudConfigPath + ControllerCFG.CloudConfigPath = tmpfile.Name() + defer func() { + ControllerCFG.CloudConfigPath = oldPath + }() + cfg := &CloudConfig{} + err = cfg.LoadCloudCFG() + assert.NoError(t, err) + + assert.Equal(t, "dGVzdC1hY2Nlc3Mta2V5", cfg.Global.AccessKeyID) + assert.Equal(t, "dGVzdC1rZXktc2VjcmV0", cfg.Global.AccessKeySecret) + assert.Equal(t, "test-cluster-id", cfg.Global.ClusterID) + assert.Equal(t, "cn-hangzhou", cfg.Global.Region) + assert.Equal(t, "vpc-123456", cfg.Global.VpcID) + assert.Equal(t, "cn-hangzhou-a:vsw-123456,cn-wulanchabu-c:vsw-234567", cfg.Global.VswitchID) + assert.Equal(t, "123456", cfg.Global.UID) + assert.Equal(t, 5, cfg.Global.ServiceMaxConcurrentReconciles) + assert.Equal(t, 2, cfg.Global.NodeMaxConcurrentReconciles) +} + +func TestCloudConfig_LoadCloudCFG_JSON(t *testing.T) { + config := ` +{ + "Global": { + "AccessKeyID": "dGVzdC1hY2Nlc3Mta2V5", + "AccessKeySecret": "dGVzdC1rZXktc2VjcmV0", + "ClusterID": "test-cluster-id", + "ResourceGroupID": "rg-123456", + "Region": "cn-hangzhou", + "RouteTableIDS": "vtb-123456", + "ServiceBackendType": "ecs", + "UID": "123456", + "VpcID": "vpc-123456", + "VswitchID": "cn-hangzhou-a:vsw-123456,cn-wulanchabu-c:vsw-234567", + "ServiceMaxConcurrentReconciles": 5, + "NodeMaxConcurrentReconciles": 2 + } +} +` + tmpfile, err := os.CreateTemp("", "cloud-config") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + _, err = tmpfile.Write([]byte(config)) + assert.NoError(t, err) + err = tmpfile.Close() + assert.NoError(t, err) + + oldPath := ControllerCFG.CloudConfigPath + ControllerCFG.CloudConfigPath = tmpfile.Name() + defer func() { + ControllerCFG.CloudConfigPath = oldPath + }() + cfg := &CloudConfig{} + err = cfg.LoadCloudCFG() + assert.NoError(t, err) + + assert.Equal(t, "dGVzdC1hY2Nlc3Mta2V5", cfg.Global.AccessKeyID) + assert.Equal(t, "dGVzdC1rZXktc2VjcmV0", cfg.Global.AccessKeySecret) + assert.Equal(t, "test-cluster-id", cfg.Global.ClusterID) + assert.Equal(t, "cn-hangzhou", cfg.Global.Region) + assert.Equal(t, "vpc-123456", cfg.Global.VpcID) + assert.Equal(t, "cn-hangzhou-a:vsw-123456,cn-wulanchabu-c:vsw-234567", cfg.Global.VswitchID) + assert.Equal(t, "123456", cfg.Global.UID) + assert.Equal(t, 5, cfg.Global.ServiceMaxConcurrentReconciles) + assert.Equal(t, 2, cfg.Global.NodeMaxConcurrentReconciles) +} + +func TestCloudConfig_LoadCloudCFG_FormatError(t *testing.T) { + config := ` +{ + "Global": { + "AccessKeyID": "" +} +` + tmpfile, err := os.CreateTemp("", "cloud-config") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + _, err = tmpfile.Write([]byte(config)) + assert.NoError(t, err) + err = tmpfile.Close() + assert.NoError(t, err) + + oldPath := ControllerCFG.CloudConfigPath + ControllerCFG.CloudConfigPath = tmpfile.Name() + defer func() { + ControllerCFG.CloudConfigPath = oldPath + }() + cfg := &CloudConfig{} + err = cfg.LoadCloudCFG() + assert.Error(t, err) +} + +func TestCloudConfig_LoadCloudCFG_FileNotFound(t *testing.T) { + oldPath := ControllerCFG.CloudConfigPath + ControllerCFG.CloudConfigPath = "/non/existent/file.yaml" + defer func() { + ControllerCFG.CloudConfigPath = oldPath + }() + + cfg := &CloudConfig{} + err := cfg.LoadCloudCFG() + assert.Error(t, err) +} + +func TestCloudConfig_PrintInfo(t *testing.T) { + cfg := &CloudConfig{} + cfg.Global.RouteTableIDS = "route-table-1,route-table-2" + cfg.Global.ResourceGroupID = "rg-12345" + cfg.Global.FeatureGates = "Feature1=true,Feature2=false" + cfg.Global.NodeMaxConcurrentReconciles = 3 + cfg.Global.ServiceMaxConcurrentReconciles = 5 + cfg.Global.RouteMaxConcurrentReconciles = 2 + + assert.NotPanics(t, func() { + cfg.PrintInfo() + }) +} diff --git a/pkg/config/controller_config.go b/pkg/config/controller_config.go index 383c5d3d1..27dabecfa 100644 --- a/pkg/config/controller_config.go +++ b/pkg/config/controller_config.go @@ -117,7 +117,7 @@ func (cfg *ControllerConfig) Validate() error { cfg.RouteReconciliationPeriod.Duration = 1 * time.Minute } - if cfg.NodeReconcileBatchSize == 0 { + if cfg.NodeReconcileBatchSize <= 0 { cfg.NodeReconcileBatchSize = 100 } diff --git a/pkg/config/controller_config_test.go b/pkg/config/controller_config_test.go new file mode 100644 index 000000000..07ac08d1e --- /dev/null +++ b/pkg/config/controller_config_test.go @@ -0,0 +1,258 @@ +package config + +import ( + "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cloud-provider/config" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestControllerConfig_Validate(t *testing.T) { + tests := []struct { + name string + config *ControllerConfig + expectError bool + }{ + { + name: "valid config", + config: &ControllerConfig{ + KubeCloudSharedConfiguration: config.KubeCloudSharedConfiguration{ + ConfigureCloudRoutes: true, + ClusterCIDR: "10.0.0.0/16", + }, + CloudConfigPath: "/tmp/cloud-config", + MaxConcurrentActions: 5, + MaxThrottlingRetryTimes: 3, + }, + expectError: false, + }, + { + name: "empty cloud config path", + config: &ControllerConfig{ + CloudConfigPath: "", + }, + expectError: true, + }, + { + name: "configure cloud routes without cluster cidr", + config: &ControllerConfig{ + KubeCloudSharedConfiguration: config.KubeCloudSharedConfiguration{ + ConfigureCloudRoutes: true, + ClusterCIDR: "", + }, + CloudConfigPath: "/tmp/cloud-config", + MaxConcurrentActions: 10, + MaxThrottlingRetryTimes: 10, + }, + expectError: true, + }, + { + name: "route reconciliation period less than 1 minute", + config: &ControllerConfig{ + KubeCloudSharedConfiguration: config.KubeCloudSharedConfiguration{ + ConfigureCloudRoutes: true, + ClusterCIDR: "10.0.0.0/16", + RouteReconciliationPeriod: metav1.Duration{Duration: 30 * time.Second}, + }, + CloudConfigPath: "/tmp/cloud-config", + MaxConcurrentActions: 10, + MaxThrottlingRetryTimes: 10, + }, + expectError: false, + }, + { + name: "negative node reconcile batch size", + config: &ControllerConfig{ + KubeCloudSharedConfiguration: config.KubeCloudSharedConfiguration{ + ConfigureCloudRoutes: true, + ClusterCIDR: "10.0.0.0/16", + }, + CloudConfigPath: "/tmp/cloud-config", + NodeReconcileBatchSize: -1, + MaxConcurrentActions: 5, + MaxThrottlingRetryTimes: 3, + }, + expectError: false, + }, + { + name: "negative node event aggregation wait seconds", + config: &ControllerConfig{ + KubeCloudSharedConfiguration: config.KubeCloudSharedConfiguration{ + ConfigureCloudRoutes: true, + ClusterCIDR: "10.0.0.0/16", + }, + CloudConfigPath: "/tmp/cloud-config", + NodeReconcileBatchSize: 10, + NodeEventAggregationWaitSeconds: -1, + MaxConcurrentActions: 5, + MaxThrottlingRetryTimes: 3, + }, + expectError: false, + }, + { + name: "zero max concurrent actions", + config: &ControllerConfig{ + KubeCloudSharedConfiguration: config.KubeCloudSharedConfiguration{ + ConfigureCloudRoutes: true, + ClusterCIDR: "10.0.0.0/16", + }, + CloudConfigPath: "/tmp/cloud-config", + MaxConcurrentActions: 0, + MaxThrottlingRetryTimes: 3, + }, + expectError: true, + }, + { + name: "zero max throttling retry times", + config: &ControllerConfig{ + KubeCloudSharedConfiguration: config.KubeCloudSharedConfiguration{ + ConfigureCloudRoutes: true, + ClusterCIDR: "10.0.0.0/16", + }, + CloudConfigPath: "/tmp/cloud-config", + MaxConcurrentActions: 5, + MaxThrottlingRetryTimes: 0, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + + // Check that route reconciliation period is at least 1 minute + if tt.config.RouteReconciliationPeriod.Duration < 1*time.Minute && tt.config.RouteReconciliationPeriod.Duration != 0 { + assert.Equal(t, 1*time.Minute, tt.config.RouteReconciliationPeriod.Duration) + } + + // Check that node reconcile batch size defaults to 100 if 0 + if tt.config.NodeReconcileBatchSize == 0 { + assert.Equal(t, 100, tt.config.NodeReconcileBatchSize) + } + } + }) + } +} + +func TestControllerConfig_BindFlags(t *testing.T) { + assert.NotPanics(t, func() { + fs := pflag.NewFlagSet("test", pflag.PanicOnError) + ControllerCFG.BindFlags(fs) + }) +} + +func TestControllerConfig_LoadControllerConfig(t *testing.T) { + // Save original args + origArgs := os.Args + defer func() { os.Args = origArgs }() + originControllerCFG := ControllerCFG + defer func() { ControllerCFG = originControllerCFG }() + + // Create a temporary cloud config file for testing + tmpFile, err := os.CreateTemp("", "cloud-config") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(testCloudConfig) + assert.NoError(t, err) + err = tmpFile.Close() + assert.NoError(t, err) + t.Logf("cloud-config is %s", tmpFile.Name()) + + tests := []struct { + name string + args []string + expectError bool + setupFunc func(*ControllerConfig) + }{ + { + name: "valid config with cloud config file", + args: []string{ + "cloud-controller-manager", + "--cloud-config=" + tmpFile.Name(), + "--cluster-cidr=10.0.0.0/16", + "--configure-cloud-routes=true", + "--max-concurrent-actions=5", + "--max-throttling-retry-times=3", + }, + expectError: false, + }, + { + name: "invalid cloud config path", + args: []string{ + "cloud-controller-manager", + "--cloud-config=/non-existent/path/to/config", + "--cluster-cidr=10.0.0.0/16", + "--configure-cloud-routes=true", + "--max-concurrent-actions=5", + "--max-throttling-retry-times=3", + }, + expectError: true, + }, + { + name: "validation fails - empty cloud config path", + args: []string{ + "cloud-controller-manager", + "--cloud-config=", + "--cluster-cidr=10.0.0.0/16", + "--configure-cloud-routes=true", + "--max-concurrent-actions=5", + "--max-throttling-retry-times=3", + }, + expectError: true, + }, + { + name: "validation fails - configure routes without cluster cidr", + args: []string{ + "cloud-controller-manager", + "--cloud-config=" + tmpFile.Name(), + "--configure-cloud-routes=true", + "--max-concurrent-actions=5", + "--max-throttling-retry-times=3", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset flag.CommandLine to avoid panic from klog.InitFlags + // This is needed because klog.InitFlags registers flags globally + // and calling it multiple times causes issues + defer func() { + if r := recover(); r != nil { + // If panic occurs due to flag re-registration, skip this test + // This is a known limitation when testing LoadControllerConfig + t.Logf("Recovered from panic (likely due to flag re-registration): %v", r) + } + }() + + os.Args = tt.args + + t.Logf("args: %+v", tt.args) + cfg := &ControllerConfig{ + CloudConfig: &CloudConfig{}, + } + if tt.setupFunc != nil { + tt.setupFunc(cfg) + } + ControllerCFG = cfg + + err := cfg.LoadControllerConfig() + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/config/feature_gates.go b/pkg/config/feature_gates.go index b4e84d9d6..e21b70d47 100644 --- a/pkg/config/feature_gates.go +++ b/pkg/config/feature_gates.go @@ -31,7 +31,7 @@ func init() { runtime.Must(utilfeature.DefaultMutableFeatureGate.Add(CloudProviderFeatureGates)) } -func BindFeatureGates(client *apiext.Clientset, features string) error { +func BindFeatureGates(client apiext.Interface, features string) error { m := make(map[string]bool) for _, s := range strings.Split(features, ",") { if len(s) == 0 { diff --git a/pkg/config/feature_gates_test.go b/pkg/config/feature_gates_test.go new file mode 100644 index 000000000..a9c26f3df --- /dev/null +++ b/pkg/config/feature_gates_test.go @@ -0,0 +1,93 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + apiextfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + "k8s.io/apimachinery/pkg/version" + utilfeature "k8s.io/apiserver/pkg/util/feature" + k8stesting "k8s.io/client-go/discovery/fake" + "k8s.io/component-base/featuregate" +) + +func TestBindFeatureGates(t *testing.T) { + tests := []struct { + name string + features string + k8sVersion string + expectError bool + validateFunc func(*testing.T) + }{ + { + name: "valid feature gates", + features: "EndpointSlice=true,IPv6DualStack=false", + k8sVersion: "v1.20.0", + expectError: false, + validateFunc: func(t *testing.T) { + assert.True(t, utilfeature.DefaultMutableFeatureGate.Enabled(EndpointSlice)) + assert.False(t, utilfeature.DefaultMutableFeatureGate.Enabled(IPv6DualStack)) + }, + }, + { + name: "invalid feature gates", + features: "EndpointSlice=true,IPv6DualStack", + k8sVersion: "v1.20.0", + expectError: true, + }, + { + name: "invalid feature gates 2", + features: "EndpointSlice=123", + k8sVersion: "v1.20.0", + expectError: true, + }, + { + name: "disable EndpointSlice for k8s < v1.20.0", + features: "EndpointSlice=true", + k8sVersion: "v1.19.0", + expectError: false, + validateFunc: func(t *testing.T) { + assert.False(t, utilfeature.DefaultMutableFeatureGate.Enabled(EndpointSlice)) + }, + }, + { + name: "disable IPv6DualStack for k8s < v1.20.0", + features: "IPv6DualStack=true", + k8sVersion: "v1.19.0", + expectError: false, + validateFunc: func(t *testing.T) { + assert.False(t, utilfeature.DefaultMutableFeatureGate.Enabled(IPv6DualStack)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetFeatureGates() + + client := apiextfake.NewSimpleClientset() + fakeDiscovery, ok := client.Discovery().(*k8stesting.FakeDiscovery) + assert.True(t, ok) + + fakeDiscovery.FakedServerVersion = &version.Info{ + GitVersion: tt.k8sVersion, + } + + err := BindFeatureGates(client, tt.features) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validateFunc != nil { + tt.validateFunc(t) + } + } + }) + } +} + +func resetFeatureGates() { + utilfeature.DefaultMutableFeatureGate = featuregate.NewFeatureGate() + _ = utilfeature.DefaultMutableFeatureGate.Add(CloudProviderFeatureGates) +} diff --git a/pkg/config/runtime_config_test.go b/pkg/config/runtime_config_test.go new file mode 100644 index 000000000..610f51305 --- /dev/null +++ b/pkg/config/runtime_config_test.go @@ -0,0 +1,12 @@ +package config + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBuildRuntimeOptions(t *testing.T) { + assert.NotPanics(t, func() { + _ = BuildRuntimeOptions(RuntimeConfig{}) + }) +} diff --git a/pkg/context/base/context_test.go b/pkg/context/base/context_test.go new file mode 100644 index 000000000..d6a5cdfc8 --- /dev/null +++ b/pkg/context/base/context_test.go @@ -0,0 +1,199 @@ +package base + +import ( + "sync" + "testing" +) + +func TestNewContext(t *testing.T) { + ctx := NewContext() + if ctx == nil { + t.Fatal("NewContext should not return nil") + } + if ctx.Ctx == nil { + t.Fatal("NewContext should initialize Ctx field") + } +} + +func TestContext_SetKV(t *testing.T) { + tests := []struct { + name string + ctx *Context + key string + value interface{} + }{ + { + name: "set value on initialized context", + ctx: &Context{Ctx: &sync.Map{}}, + key: "testKey", + value: "testValue", + }, + { + name: "set value on nil context", + ctx: &Context{}, + key: "testKey", + value: "testValue", + }, + { + name: "set nil value", + ctx: &Context{Ctx: &sync.Map{}}, + key: "testKey", + value: nil, + }, + { + name: "set complex value", + ctx: &Context{Ctx: &sync.Map{}}, + key: "complexKey", + value: struct{ Name string }{Name: "test"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.ctx.SetKV(tt.key, tt.value) + + if tt.ctx.Ctx == nil { + t.Fatal("SetKV should initialize Ctx if nil") + } + + val, ok := tt.ctx.Ctx.Load(tt.key) + if !ok { + t.Errorf("key %s not found after SetKV", tt.key) + } + if val != tt.value { + t.Errorf("value mismatch: got %v, want %v", val, tt.value) + } + }) + } +} + +func TestContext_Value(t *testing.T) { + tests := []struct { + name string + ctx *Context + key string + wantValue interface{} + wantOk bool + }{ + { + name: "get existing value", + ctx: func() *Context { + c := &Context{Ctx: &sync.Map{}} + c.Ctx.Store("key1", "value1") + return c + }(), + key: "key1", + wantValue: "value1", + wantOk: true, + }, + { + name: "get non-existing value", + ctx: &Context{Ctx: &sync.Map{}}, + key: "nonExistingKey", + wantValue: nil, + wantOk: false, + }, + { + name: "get from nil context", + ctx: &Context{}, + key: "anyKey", + wantValue: nil, + wantOk: false, + }, + { + name: "get nil value", + ctx: func() *Context { + c := &Context{Ctx: &sync.Map{}} + c.Ctx.Store("nilKey", nil) + return c + }(), + key: "nilKey", + wantValue: nil, + wantOk: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, gotOk := tt.ctx.Value(tt.key) + if gotOk != tt.wantOk { + t.Errorf("Value() gotOk = %v, want %v", gotOk, tt.wantOk) + } + if gotValue != tt.wantValue { + t.Errorf("Value() gotValue = %v, want %v", gotValue, tt.wantValue) + } + }) + } +} + +func TestContext_Range(t *testing.T) { + tests := []struct { + name string + ctx *Context + setup func(*Context) + wantKeys []string + }{ + { + name: "range over multiple entries", + ctx: &Context{Ctx: &sync.Map{}}, + setup: func(c *Context) { + c.SetKV("key1", "value1") + c.SetKV("key2", "value2") + c.SetKV("key3", "value3") + }, + wantKeys: []string{"key1", "key2", "key3"}, + }, + { + name: "range over empty context", + ctx: &Context{Ctx: &sync.Map{}}, + setup: func(c *Context) {}, + wantKeys: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup(tt.ctx) + + var keys []string + tt.ctx.Range(func(key, value interface{}) bool { + keys = append(keys, key.(string)) + return true + }) + + if len(keys) != len(tt.wantKeys) { + t.Errorf("Range() got %d keys, want %d keys", len(keys), len(tt.wantKeys)) + return + } + + // Convert to map for easier comparison + keyMap := make(map[string]bool) + for _, k := range keys { + keyMap[k] = true + } + + for _, wantKey := range tt.wantKeys { + if !keyMap[wantKey] { + t.Errorf("Range() missing key %s", wantKey) + } + } + }) + } +} + +func TestContext_RangeEarlyExit(t *testing.T) { + ctx := &Context{Ctx: &sync.Map{}} + ctx.SetKV("key1", "value1") + ctx.SetKV("key2", "value2") + ctx.SetKV("key3", "value3") + + count := 0 + ctx.Range(func(key, value interface{}) bool { + count++ + return count < 2 // Stop after 2 iterations + }) + + if count != 2 { + t.Errorf("Range() should stop early, got %d iterations, want 2", count) + } +} diff --git a/pkg/context/shared/sharedcontext_test.go b/pkg/context/shared/sharedcontext_test.go new file mode 100644 index 000000000..65e4cd562 --- /dev/null +++ b/pkg/context/shared/sharedcontext_test.go @@ -0,0 +1,143 @@ +package shared + +import ( + "testing" + + "k8s.io/cloud-provider-alibaba-cloud/pkg/context/base" + "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" +) + +func TestNewSharedContext(t *testing.T) { + mockProvider := &vmock.MockCloud{} + + ctx := NewSharedContext(mockProvider) + + if ctx == nil { + t.Fatal("NewSharedContext should not return nil") + } + + // Verify provider is set + provider := ctx.Provider() + if provider == nil { + t.Fatal("Provider should not be nil after NewSharedContext") + } + + if provider != mockProvider { + t.Error("Provider returned does not match the one passed to NewSharedContext") + } +} + +func TestSharedContext_Provider(t *testing.T) { + tests := []struct { + name string + setup func() *SharedContext + wantNil bool + expectPanic bool + }{ + { + name: "provider exists", + setup: func() *SharedContext { + mockProvider := &vmock.MockCloud{} + return NewSharedContext(mockProvider) + }, + wantNil: false, + }, + { + name: "provider not set", + setup: func() *SharedContext { + return &SharedContext{ + Context: base.Context{}, + } + }, + wantNil: true, + }, + { + name: "provider set to nil", + setup: func() *SharedContext { + ctx := &SharedContext{} + ctx.SetKV(Provider, nil) + return ctx + }, + wantNil: true, + expectPanic: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := tt.setup() + + if tt.expectPanic { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic but did not occur") + } + }() + _ = ctx.Provider() + return + } + + provider := ctx.Provider() + + if tt.wantNil && provider != nil { + t.Errorf("Provider() should return nil, got %v", provider) + } + if !tt.wantNil && provider == nil { + t.Error("Provider() should not return nil") + } + }) + } +} + +func TestSharedContext_SetAndGetProvider(t *testing.T) { + mockProvider1 := &vmock.MockCloud{} + mockProvider2 := &vmock.MockCloud{} + + ctx := NewSharedContext(mockProvider1) + + // Verify initial provider + if ctx.Provider() != mockProvider1 { + t.Error("Initial provider mismatch") + } + + // Update provider + ctx.SetKV(Provider, mockProvider2) + + // Verify updated provider + if ctx.Provider() != mockProvider2 { + t.Error("Updated provider mismatch") + } +} + +func TestSharedContext_InheritFromBaseContext(t *testing.T) { + mockProvider := &vmock.MockCloud{} + ctx := NewSharedContext(mockProvider) + + // Test that SharedContext inherits base.Context methods + testKey := "testKey" + testValue := "testValue" + + // Test SetKV + ctx.SetKV(testKey, testValue) + + // Test Value + val, ok := ctx.Value(testKey) + if !ok { + t.Error("Value() should return true for existing key") + } + if val != testValue { + t.Errorf("Value() = %v, want %v", val, testValue) + } + + // Test Range + count := 0 + ctx.Range(func(key, value interface{}) bool { + count++ + return true + }) + + // Should have at least Provider and testKey + if count < 2 { + t.Errorf("Range() should iterate over at least 2 entries, got %d", count) + } +} diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go new file mode 100644 index 000000000..af97f1122 --- /dev/null +++ b/pkg/controller/controller_test.go @@ -0,0 +1,54 @@ +package controller + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/cloud-provider-alibaba-cloud/pkg/context/shared" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +func TestAddToManager(t *testing.T) { + originalMap := controllerMap + defer func() { + controllerMap = originalMap + }() + + t.Run("success with multiple controllers", func(t *testing.T) { + called := make([]string, 0, 2) + controllerMap = map[string]func(manager.Manager, *shared.SharedContext) error{ + "node": func(manager.Manager, *shared.SharedContext) error { + called = append(called, "node") + return nil + }, + "route": func(manager.Manager, *shared.SharedContext) error { + called = append(called, "route") + return nil + }, + } + + err := AddToManager(nil, nil, []string{"node", "route"}) + assert.NoError(t, err) + assert.Equal(t, []string{"node", "route"}, called) + }) + + t.Run("unknown controller returns error", func(t *testing.T) { + controllerMap = map[string]func(manager.Manager, *shared.SharedContext) error{} + + err := AddToManager(nil, nil, []string{"unknown"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot find controller unknown") + }) + + t.Run("controller add failed returns error", func(t *testing.T) { + controllerMap = map[string]func(manager.Manager, *shared.SharedContext) error{ + "node": func(manager.Manager, *shared.SharedContext) error { + return errors.New("add node failed") + }, + } + + err := AddToManager(nil, nil, []string{"node"}) + assert.EqualError(t, err, "add node failed") + }) +} diff --git a/pkg/controller/helper/endpoint_utils_test.go b/pkg/controller/helper/endpoint_utils_test.go new file mode 100644 index 000000000..0f28ae254 --- /dev/null +++ b/pkg/controller/helper/endpoint_utils_test.go @@ -0,0 +1,149 @@ +package helper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestLogEndpoints(t *testing.T) { + tests := []struct { + name string + endpoint *v1.Endpoints + }{ + { + name: "nil endpoints", + endpoint: nil, + }, + { + name: "empty endpoints", + endpoint: &v1.Endpoints{}, + }, + { + name: "endpoints with addresses", + endpoint: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + {IP: "192.168.1.1"}, + {IP: "192.168.1.2"}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.NotPanics(t, func() { + _ = LogEndpoints(tt.endpoint) + }) + }) + } +} + +func TestLogEndpointSlice(t *testing.T) { + tests := []struct { + name string + es *discovery.EndpointSlice + }{ + { + name: "nil endpoint slice", + es: nil, + }, + { + name: "empty endpoint slice", + es: &discovery.EndpointSlice{}, + }, + { + name: "endpoint slice with endpoints", + es: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"192.168.1.1", "192.168.1.2"}, + }, + { + Addresses: []string{"192.168.1.3"}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.NotPanics(t, func() { + _ = LogEndpointSlice(tt.es) + }) + }) + } +} + +func TestLogEndpointSliceList(t *testing.T) { + tests := []struct { + name string + esList []discovery.EndpointSlice + }{ + { + name: "nil endpoint slice list", + esList: nil, + }, + { + name: "empty endpoint slice list", + esList: []discovery.EndpointSlice{}, + }, + { + name: "endpoint slice list with endpoints", + esList: []discovery.EndpointSlice{ + { + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"192.168.1.1", "192.168.1.2"}, + }, + }, + }, + { + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"192.168.1.3"}, + }, + }, + }, + }, + }, + { + name: "endpoint slice list with ready and not ready endpoints", + esList: []discovery.EndpointSlice{ + { + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"192.168.1.1"}, + Conditions: discovery.EndpointConditions{ + Ready: func() *bool { r := true; return &r }(), + }, + }, + { + Addresses: []string{"192.168.1.2"}, + Conditions: discovery.EndpointConditions{ + Ready: func() *bool { r := false; return &r }(), + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.NotPanics(t, func() { + _ = LogEndpointSliceList(tt.esList) + }) + }) + } +} diff --git a/pkg/controller/helper/event_test.go b/pkg/controller/helper/event_test.go new file mode 100644 index 000000000..6ffa42357 --- /dev/null +++ b/pkg/controller/helper/event_test.go @@ -0,0 +1,47 @@ +package helper + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + utilerrors "k8s.io/apimachinery/pkg/util/errors" +) + +func TestGetLogMessage(t *testing.T) { + t.Run("nil error", func(t *testing.T) { + assert.Equal(t, "", GetLogMessage(nil)) + }) + + t.Run("simple error", func(t *testing.T) { + err := errors.New("simple error message") + assert.Equal(t, "simple error message", GetLogMessage(err)) + }) + + t.Run("aggregate error with multiple errors", func(t *testing.T) { + err1 := errors.New("first error") + err2 := errors.New("second error") + agg := utilerrors.NewAggregate([]error{err1, err2}) + assert.Equal(t, "first error", GetLogMessage(agg)) + }) + + t.Run("aggregate error with no errors", func(t *testing.T) { + agg := utilerrors.NewAggregate([]error{}) + assert.Equal(t, "", GetLogMessage(agg)) + }) + + t.Run("sdk error with message", func(t *testing.T) { + err := errors.New("some error [SDKError] Message: SDK error message content, more details") + assert.Equal(t, "Message: SDK error message content, more details", GetLogMessage(err)) + }) + + t.Run("sdk error without message pattern", func(t *testing.T) { + err := errors.New("some error [SDKError] other error content") + assert.Equal(t, "some error [SDKError] other error content", GetLogMessage(err)) + }) + + t.Run("error without sdk pattern", func(t *testing.T) { + err := errors.New("regular error without SDK tag") + assert.Equal(t, "regular error without SDK tag", GetLogMessage(err)) + }) +} diff --git a/pkg/controller/helper/finalizer_test.go b/pkg/controller/helper/finalizer_test.go new file mode 100644 index 000000000..f574f0d46 --- /dev/null +++ b/pkg/controller/helper/finalizer_test.go @@ -0,0 +1,198 @@ +package helper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestHasFinalizer(t *testing.T) { + t.Run("object has no finalizers", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{}, + }, + } + assert.False(t, HasFinalizer(svc, "test-finalizer")) + }) + + t.Run("object has the specified finalizer", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{"test-finalizer", "another-finalizer"}, + }, + } + assert.True(t, HasFinalizer(svc, "test-finalizer")) + }) + + t.Run("object has other finalizers but not the specified one", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{"another-finalizer", "yet-another-finalizer"}, + }, + } + assert.False(t, HasFinalizer(svc, "test-finalizer")) + }) +} + +func TestAddFinalizers(t *testing.T) { + scheme := runtime.NewScheme() + _ = v1.AddToScheme(scheme) + + t.Run("add new finalizer to object without finalizers", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(svc).Build() + manager := NewDefaultFinalizerManager(c) + + err := manager.AddFinalizers(context.Background(), svc, "test-finalizer") + assert.NoError(t, err) + + // Get the updated object + updatedSvc := &v1.Service{} + err = c.Get(context.Background(), client.ObjectKeyFromObject(svc), updatedSvc) + assert.NoError(t, err) + assert.True(t, HasFinalizer(updatedSvc, "test-finalizer")) + }) + + t.Run("add existing finalizer to object", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{"test-finalizer"}, + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(svc).Build() + manager := NewDefaultFinalizerManager(c) + + err := manager.AddFinalizers(context.Background(), svc, "test-finalizer") + assert.NoError(t, err) + + // Get the updated object + updatedSvc := &v1.Service{} + err = c.Get(context.Background(), client.ObjectKeyFromObject(svc), updatedSvc) + assert.NoError(t, err) + assert.Equal(t, 1, len(updatedSvc.Finalizers)) + assert.True(t, HasFinalizer(updatedSvc, "test-finalizer")) + }) + + t.Run("add multiple finalizers", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(svc).Build() + manager := NewDefaultFinalizerManager(c) + + err := manager.AddFinalizers(context.Background(), svc, "finalizer-1", "finalizer-2") + assert.NoError(t, err) + + // Get the updated object + updatedSvc := &v1.Service{} + err = c.Get(context.Background(), client.ObjectKeyFromObject(svc), updatedSvc) + assert.NoError(t, err) + assert.True(t, HasFinalizer(updatedSvc, "finalizer-1")) + assert.True(t, HasFinalizer(updatedSvc, "finalizer-2")) + assert.Equal(t, 2, len(updatedSvc.Finalizers)) + }) +} + +func TestRemoveFinalizers(t *testing.T) { + scheme := runtime.NewScheme() + _ = v1.AddToScheme(scheme) + + t.Run("remove existing finalizer", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{"test-finalizer", "another-finalizer"}, + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(svc).Build() + manager := NewDefaultFinalizerManager(c) + + err := manager.RemoveFinalizers(context.Background(), svc, "test-finalizer") + assert.NoError(t, err) + + // Get the updated object + updatedSvc := &v1.Service{} + err = c.Get(context.Background(), client.ObjectKeyFromObject(svc), updatedSvc) + assert.NoError(t, err) + assert.False(t, HasFinalizer(updatedSvc, "test-finalizer")) + assert.True(t, HasFinalizer(updatedSvc, "another-finalizer")) + assert.Equal(t, 1, len(updatedSvc.Finalizers)) + }) + + t.Run("remove non-existing finalizer", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{"another-finalizer"}, + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(svc).Build() + manager := NewDefaultFinalizerManager(c) + + err := manager.RemoveFinalizers(context.Background(), svc, "test-finalizer") + assert.NoError(t, err) + + // Get the updated object + updatedSvc := &v1.Service{} + err = c.Get(context.Background(), client.ObjectKeyFromObject(svc), updatedSvc) + assert.NoError(t, err) + assert.False(t, HasFinalizer(updatedSvc, "test-finalizer")) + assert.True(t, HasFinalizer(updatedSvc, "another-finalizer")) + assert.Equal(t, 1, len(updatedSvc.Finalizers)) + }) + + t.Run("remove multiple finalizers", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{"finalizer-1", "finalizer-2", "finalizer-3"}, + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(svc).Build() + manager := NewDefaultFinalizerManager(c) + + err := manager.RemoveFinalizers(context.Background(), svc, "finalizer-1", "finalizer-2") + assert.NoError(t, err) + + // Get the updated object + updatedSvc := &v1.Service{} + err = c.Get(context.Background(), client.ObjectKeyFromObject(svc), updatedSvc) + assert.NoError(t, err) + assert.False(t, HasFinalizer(updatedSvc, "finalizer-1")) + assert.False(t, HasFinalizer(updatedSvc, "finalizer-2")) + assert.True(t, HasFinalizer(updatedSvc, "finalizer-3")) + assert.Equal(t, 1, len(updatedSvc.Finalizers)) + }) +} diff --git a/pkg/controller/helper/net_utils_test.go b/pkg/controller/helper/net_utils_test.go new file mode 100644 index 000000000..61e6ea8f4 --- /dev/null +++ b/pkg/controller/helper/net_utils_test.go @@ -0,0 +1,99 @@ +package helper + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsIPv4(t *testing.T) { + tests := []struct { + name string + address string + expected bool + }{ + { + name: "valid IPv4 address", + address: "192.168.1.1", + expected: true, + }, + { + name: "invalid IPv4 address with too many dots", + address: "192.168.1.1.1", + expected: true, // Still counts as IPv4 as it has less than 2 colons + }, + { + name: "IPv6 address", + address: "2001:db8::1", + expected: false, + }, + { + name: "IPv6 address with brackets", + address: "[2001:db8::1]", + expected: false, + }, + { + name: "localhost IPv4", + address: "127.0.0.1", + expected: true, + }, + { + name: "localhost IPv6", + address: "::1", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsIPv4(tt.address) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsIPv6(t *testing.T) { + tests := []struct { + name string + address string + expected bool + }{ + { + name: "valid IPv4 address", + address: "192.168.1.1", + expected: false, + }, + { + name: "IPv6 address", + address: "2001:db8::1", + expected: true, + }, + { + name: "IPv6 address with brackets", + address: "[2001:db8::1]", + expected: true, + }, + { + name: "localhost IPv4", + address: "127.0.0.1", + expected: false, + }, + { + name: "localhost IPv6", + address: "::1", + expected: true, + }, + { + name: "IPv4-mapped IPv6 address", + address: "::ffff:192.0.2.1", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsIPv6(tt.address) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/controller/helper/node_utils.go b/pkg/controller/helper/node_utils.go index b0af07ade..99e66f55f 100644 --- a/pkg/controller/helper/node_utils.go +++ b/pkg/controller/helper/node_utils.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "strings" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime" @@ -11,7 +13,6 @@ import ( "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" - "strings" ) const ( @@ -23,15 +24,14 @@ const ( const ( LabelNodeRoleMaster = "node-role.kubernetes.io/master" LabelNodeTypeVK = "virtual-kubelet" - // LabelNodeExcludeBalancer specifies that the node should be - // exclude from loadbalancers created by a cloud provider. - LabelNodeExcludeBalancerDeprecated = "alpha.service-controller.kubernetes.io/exclude-balancer" + // LabelNodeExcludeBalancer specifies that the node should be excluded from loadbalancers created by a cloud provider. LabelNodeExcludeBalancer = v1.LabelNodeExcludeBalancers + LabelNodeExcludeBalancerDeprecated = "alpha.service-controller.kubernetes.io/exclude-balancer" // ToBeDeletedTaint is a taint used by the CLuster Autoscaler before marking a node for deletion. // Details in https://github.com/kubernetes/cloud-provider/blob/5bb9b27442bcb2613a9ca4046c89109de4435824/controllers/service/controller.go#L58 ToBeDeletedTaint = "ToBeDeletedByClusterAutoscaler" - // LabelNodeExcludeNodeDeprecated specifies that the node should be exclude from CCM + // LabelNodeExcludeNodeDeprecated specifies that the node should be excluded from CCM LabelNodeExcludeNodeDeprecated = "service.beta.kubernetes.io/exclude-node" LabelNodeExcludeNode = "service.alibabacloud.com/exclude-node" diff --git a/pkg/controller/helper/node_utils_test.go b/pkg/controller/helper/node_utils_test.go index 5f1f1c2a7..6d64155d4 100644 --- a/pkg/controller/helper/node_utils_test.go +++ b/pkg/controller/helper/node_utils_test.go @@ -2,55 +2,504 @@ package helper import ( "context" + "fmt" + "testing" + "time" + "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" ) func TestPatchM(t *testing.T) { - diff := func(copy runtime.Object) (client.Object, error) { - nins := copy.(*v1.Node) - condition, ok := FindCondition(nins.Status.Conditions, v1.NodeNetworkUnavailable) - condition.Type = v1.NodeNetworkUnavailable - condition.Status = v1.ConditionFalse - condition.Reason = "RouteCreated" - condition.Message = "RouteController created a route" - - if !ok { - nins.Status.Conditions = append(nins.Status.Conditions, *condition) - } - return nins, nil + t.Run("patch status", func(t *testing.T) { + diff := func(copy runtime.Object) (client.Object, error) { + nins := copy.(*v1.Node) + condition, ok := FindCondition(nins.Status.Conditions, v1.NodeNetworkUnavailable) + condition.Type = v1.NodeNetworkUnavailable + condition.Status = v1.ConditionFalse + condition.Reason = "RouteCreated" + condition.Message = "RouteController created a route" + + if !ok { + nins.Status.Conditions = append(nins.Status.Conditions, *condition) + } + return nins, nil + } + + mclient := getFakeKubeClient() + node1 := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + Labels: map[string]string{"app": "nginx"}, + }, + } + + err := PatchM(mclient, node1, diff, PatchStatus) + if err != nil { + t.Error(err) + } + node1Patched := &v1.Node{} + _ = mclient.Get(context.TODO(), client.ObjectKey{ + Name: node1.Name, + }, node1Patched) + + for _, con := range node1Patched.Status.Conditions { + if con.Type == v1.NodeNetworkUnavailable { + assert.Equal(t, v1.ConditionFalse, con.Status) + return + } + } + t.Error("Fail to patch node network status") + }) + + t.Run("patch all", func(t *testing.T) { + diff := func(copy runtime.Object) (client.Object, error) { + nins := copy.(*v1.Node) + + // Modify spec part + nins.Spec.Unschedulable = true + + // Modify status part + nins.Status.Conditions[0].Status = v1.ConditionFalse + nins.Status.Conditions[0].Reason = "TestPatchAll" + + return nins, nil + } + + mclient := getFakeKubeClient() + node1 := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + Labels: map[string]string{"app": "nginx"}, + }, + Spec: v1.NodeSpec{ + Unschedulable: false, + }, + } + + err := PatchM(mclient, node1, diff, PatchAll) + assert.NoError(t, err) + + // Verify updated node + node1Patched := &v1.Node{} + err = mclient.Get(context.TODO(), client.ObjectKey{ + Name: node1.Name, + }, node1Patched) + assert.NoError(t, err) + + // Verify spec is updated + assert.True(t, node1Patched.Spec.Unschedulable) + + // Verify status is updated + assert.Equal(t, v1.ConditionFalse, node1Patched.Status.Conditions[0].Status) + assert.Equal(t, "TestPatchAll", node1Patched.Status.Conditions[0].Reason) + }) + + t.Run("patch spec only", func(t *testing.T) { + diff := func(copy runtime.Object) (client.Object, error) { + nins := copy.(*v1.Node) + nins.Spec.Unschedulable = true + return nins, nil + } + + mclient := getFakeKubeClient() + node1 := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + Labels: map[string]string{"app": "nginx"}, + }, + Spec: v1.NodeSpec{ + Unschedulable: false, + }, + } + + err := PatchM(mclient, node1, diff, PatchSpec) + assert.NoError(t, err) + + node1Patched := &v1.Node{} + err = mclient.Get(context.TODO(), client.ObjectKey{ + Name: node1.Name, + }, node1Patched) + assert.NoError(t, err) + assert.True(t, node1Patched.Spec.Unschedulable) + }) + + t.Run("empty patch - no changes", func(t *testing.T) { + diff := func(copy runtime.Object) (client.Object, error) { + // Return the same object without changes + return copy.(*v1.Node), nil + } + + mclient := getFakeKubeClient() + node1 := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + Labels: map[string]string{"app": "nginx"}, + }, + } + + err := PatchM(mclient, node1, diff, PatchAll) + assert.NoError(t, err) + }) + + t.Run("getter returns error", func(t *testing.T) { + diff := func(copy runtime.Object) (client.Object, error) { + return nil, fmt.Errorf("getter error") + } + + mclient := getFakeKubeClient() + node1 := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + Labels: map[string]string{"app": "nginx"}, + }, + } + + err := PatchM(mclient, node1, diff, PatchAll) + assert.Error(t, err) + assert.Contains(t, err.Error(), "get object diff patch") + }) + + t.Run("node not found", func(t *testing.T) { + diff := func(copy runtime.Object) (client.Object, error) { + nins := copy.(*v1.Node) + nins.Spec.Unschedulable = true + return nins, nil + } + + mclient := getFakeKubeClient() + node1 := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "non-existent-node", + Labels: map[string]string{"app": "nginx"}, + }, + } + + err := PatchM(mclient, node1, diff, PatchAll) + assert.Error(t, err) + assert.Contains(t, err.Error(), "get origin object") + }) +} + +func TestNodeFromProviderID(t *testing.T) { + cases := []struct { + name string + providerID string + expectError bool + expectRegion string + expectID string + }{ + { + name: "normal provider id", + providerID: "cn-hangzhou.i-123456", + expectRegion: "cn-hangzhou", + expectID: "i-123456", + }, + { + name: "malformed provider id", + providerID: "cn-hangzhou123456", + expectError: true, + }, + { + name: "provider with alicloud://", + providerID: "alicloud://cn-hangzhou.i-123456", + expectRegion: "cn-hangzhou", + expectID: "i-123456", + }, + { + name: "malformed provider with alicloud://", + providerID: "alicloud://cn-hangzhou123456", + expectError: true, + }, } - mclient := getFakeKubeClient() - node1 := &v1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: "node-1", - Labels: map[string]string{"app": "nginx"}, + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + region, id, err := NodeFromProviderID(tt.providerID) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectRegion, region) + assert.Equal(t, tt.expectID, id) + } + }) + } +} + +func TestIsExcludedNode(t *testing.T) { + cases := []struct { + name string + node *v1.Node + expectExcluded bool + }{ + { + name: "normal node", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, + expectExcluded: false, + }, + { + name: "nil node", + node: nil, + expectExcluded: false, + }, + { + name: "node with exclude node label", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{LabelNodeExcludeNode: "true"}}}, + expectExcluded: true, + }, + { + name: "node with deprecated exclude node label", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{LabelNodeExcludeNodeDeprecated: "true"}}}, + expectExcluded: true, + }, + { + name: "hybrid node", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}, Spec: v1.NodeSpec{ProviderID: "ack-hybrid://cn-hangzhou/test"}}, + expectExcluded: true, }, } - err := PatchM(mclient, node1, diff, PatchStatus) - if err != nil { - t.Error(err) + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expectExcluded, IsExcludedNode(tt.node)) + }) } - node1Patched := &v1.Node{} - _ = mclient.Get(context.TODO(), client.ObjectKey{ - Name: node1.Name, - }, node1Patched) +} - for _, con := range node1Patched.Status.Conditions { - if con.Type == v1.NodeNetworkUnavailable { - assert.Equal(t, v1.ConditionFalse, con.Status) - return - } +func TestIsNodeExcludeFromLoadBalancer(t *testing.T) { + cases := []struct { + name string + node *v1.Node + expectExcluded bool + }{ + { + name: "normal node", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, + expectExcluded: false, + }, + { + name: "node with exclude balancer label", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{LabelNodeExcludeBalancer: "true"}}}, + expectExcluded: true, + }, + { + name: "node with deprecated exclude balancer label", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{LabelNodeExcludeBalancerDeprecated: "true"}}}, + expectExcluded: true, + }, + { + name: "node with excluded label", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{LabelNodeExcludeNode: "true"}}}, + expectExcluded: true, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expectExcluded, IsNodeExcludeFromLoadBalancer(tt.node)) + }) } - t.Error("Fail to patch node network status") +} + +func TestPatchNodeStatus(t *testing.T) { + t.Run("update node status normally", func(t *testing.T) { + mclient := getFakeKubeClient() + testNode := &v1.Node{} + err := mclient.Get(context.TODO(), client.ObjectKey{Name: "node-1"}, testNode) + assert.NoError(t, err) + + getter := func(node *v1.Node) (*v1.Node, error) { + node.Status.Conditions[0].Status = v1.ConditionFalse + node.Status.Conditions[0].Reason = "TestReason" + return node, nil + } + + err = PatchNodeStatus(mclient, testNode, getter) + assert.NoError(t, err) + + updatedNode := &v1.Node{} + err = mclient.Get(context.TODO(), client.ObjectKey{Name: testNode.Name}, updatedNode) + assert.NoError(t, err) + assert.Equal(t, v1.ConditionFalse, updatedNode.Status.Conditions[0].Status) + assert.Equal(t, "TestReason", updatedNode.Status.Conditions[0].Reason) + }) + + t.Run("update node addresses", func(t *testing.T) { + mclient := getFakeKubeClient() + testNode := &v1.Node{} + err := mclient.Get(context.TODO(), client.ObjectKey{Name: "node-1"}, testNode) + assert.NoError(t, err) + + getter := func(node *v1.Node) (*v1.Node, error) { + node.Status.Addresses = append(node.Status.Addresses, v1.NodeAddress{ + Type: v1.NodeInternalIP, + Address: "10.0.0.1", + }) + return node, nil + } + + err = PatchNodeStatus(mclient, testNode, getter) + assert.NoError(t, err) + + updatedNode := &v1.Node{} + err = mclient.Get(context.TODO(), client.ObjectKey{Name: testNode.Name}, updatedNode) + assert.NoError(t, err) + assert.Len(t, updatedNode.Status.Addresses, 1) + assert.Equal(t, v1.NodeInternalIP, updatedNode.Status.Addresses[0].Type) + assert.Equal(t, "10.0.0.1", updatedNode.Status.Addresses[0].Address) + }) + + t.Run("add ipv6 address to node", func(t *testing.T) { + mclient := getFakeKubeClient() + testNode := &v1.Node{} + err := mclient.Get(context.TODO(), client.ObjectKey{Name: "node-1"}, testNode) + assert.NoError(t, err) + + getter := func(node *v1.Node) (*v1.Node, error) { + node.Status.Addresses = []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "10.0.0.1", + }, + } + return node, nil + } + + err = PatchNodeStatus(mclient, testNode, getter) + assert.NoError(t, err) + + updatedNode := &v1.Node{} + err = mclient.Get(context.TODO(), client.ObjectKey{Name: testNode.Name}, updatedNode) + assert.NoError(t, err) + assert.Len(t, updatedNode.Status.Addresses, 1) + assert.Equal(t, v1.NodeInternalIP, updatedNode.Status.Addresses[0].Type) + assert.Equal(t, "10.0.0.1", updatedNode.Status.Addresses[0].Address) + + getter = func(node *v1.Node) (*v1.Node, error) { + node.Status.Addresses = []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "10.0.0.1", + }, + { + Type: v1.NodeInternalIP, + Address: "2001:db8::1", + }, + } + return node, nil + } + + err = PatchNodeStatus(mclient, testNode, getter) + assert.NoError(t, err) + + updatedNode = &v1.Node{} + err = mclient.Get(context.TODO(), client.ObjectKey{Name: testNode.Name}, updatedNode) + assert.Len(t, updatedNode.Status.Addresses, 2) + assert.Equal(t, v1.NodeInternalIP, updatedNode.Status.Addresses[1].Type) + assert.Equal(t, "2001:db8::1", updatedNode.Status.Addresses[1].Address) + }) + + t.Run("getter returns error", func(t *testing.T) { + mclient := getFakeKubeClient() + testNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + } + + getter := func(node *v1.Node) (*v1.Node, error) { + return nil, fmt.Errorf("getter error") + } + + err := PatchNodeStatus(mclient, testNode, getter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "get object diff patch") + }) + + t.Run("node not found", func(t *testing.T) { + mclient := getFakeKubeClient() + testNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "non-existent-node", + }, + } + + getter := func(node *v1.Node) (*v1.Node, error) { + node.Status.Conditions[0].Status = v1.ConditionFalse + return node, nil + } + + err := PatchNodeStatus(mclient, testNode, getter) + assert.Error(t, err) + assert.Contains(t, err.Error(), "get origin object") + }) + + t.Run("no changes to node status", func(t *testing.T) { + mclient := getFakeKubeClient() + testNode := &v1.Node{} + err := mclient.Get(context.TODO(), client.ObjectKey{Name: "node-1"}, testNode) + assert.NoError(t, err) + + originalStatus := testNode.Status.DeepCopy() + getter := func(node *v1.Node) (*v1.Node, error) { + // Return node without changes + return node, nil + } + + err = PatchNodeStatus(mclient, testNode, getter) + assert.NoError(t, err) + + // Verify status hasn't changed + updatedNode := &v1.Node{} + err = mclient.Get(context.TODO(), client.ObjectKey{Name: testNode.Name}, updatedNode) + assert.NoError(t, err) + assert.Equal(t, originalStatus.Conditions, updatedNode.Status.Conditions) + }) +} + +func TestFixupPatchForNodeStatusAddresses(t *testing.T) { + t.Run("normal patch fixup", func(t *testing.T) { + patchBytes := []byte(`{"status": {"conditions": [{"type": "Ready", "status": "True"}]}}`) + addresses := []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + {Type: v1.NodeExternalIP, Address: "1.2.3.4"}, + } + + result, err := fixupPatchForNodeStatusAddresses(patchBytes, addresses) + assert.NoError(t, err) + assert.Contains(t, string(result), `"addresses"`) + assert.Contains(t, string(result), `"192.168.1.1"`) + assert.Contains(t, string(result), `"1.2.3.4"`) + assert.Contains(t, string(result), `"$patch":"replace"`) + }) + + t.Run("empty patch", func(t *testing.T) { + patchBytes := []byte(`{}`) + addresses := []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + } + + result, err := fixupPatchForNodeStatusAddresses(patchBytes, addresses) + assert.NoError(t, err) + assert.Contains(t, string(result), `"addresses"`) + assert.Contains(t, string(result), `"192.168.1.1"`) + assert.Contains(t, string(result), `"$patch":"replace"`) + }) + + t.Run("invalid json", func(t *testing.T) { + patchBytes := []byte(`{invalid json}`) + addresses := []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + } + _, err := fixupPatchForNodeStatusAddresses(patchBytes, addresses) + assert.Error(t, err) + }) } func getFakeKubeClient() client.Client { @@ -87,3 +536,263 @@ func getFakeKubeClient() client.Client { objs := []runtime.Object{nodeList} return fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() } + +func TestFindCondition(t *testing.T) { + conditions := []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + { + Type: v1.NodeMemoryPressure, + Status: v1.ConditionFalse, + }, + } + + t.Run("find existing condition", func(t *testing.T) { + condition, found := FindCondition(conditions, v1.NodeReady) + assert.True(t, found) + assert.Equal(t, v1.NodeReady, condition.Type) + assert.Equal(t, v1.ConditionTrue, condition.Status) + }) + + t.Run("find non-existing condition", func(t *testing.T) { + condition, found := FindCondition(conditions, v1.NodeDiskPressure) + assert.False(t, found) + assert.Equal(t, v1.NodeCondition{}, *condition) + }) + + t.Run("empty conditions", func(t *testing.T) { + condition, found := FindCondition([]v1.NodeCondition{}, v1.NodeReady) + assert.False(t, found) + assert.Equal(t, v1.NodeCondition{}, *condition) + }) + + t.Run("multiple same type returns latest LastHeartbeatTime", func(t *testing.T) { + oldTime := metav1.NewTime(time.Now().Add(-time.Hour)) + newTime := metav1.NewTime(time.Now()) + conds := []v1.NodeCondition{ + {Type: v1.NodeReady, Status: v1.ConditionTrue, LastHeartbeatTime: oldTime}, + {Type: v1.NodeReady, Status: v1.ConditionFalse, LastHeartbeatTime: newTime}, + } + condition, found := FindCondition(conds, v1.NodeReady) + assert.True(t, found) + assert.True(t, condition.LastHeartbeatTime.Equal(&newTime)) + assert.Equal(t, v1.ConditionFalse, condition.Status) + }) +} + +func TestGetNodeCondition(t *testing.T) { + node := &v1.Node{ + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + { + Type: v1.NodeMemoryPressure, + Status: v1.ConditionFalse, + }, + }, + }, + } + + t.Run("find existing condition", func(t *testing.T) { + condition := GetNodeCondition(node, v1.NodeReady) + assert.NotNil(t, condition) + assert.Equal(t, v1.NodeReady, condition.Type) + assert.Equal(t, v1.ConditionTrue, condition.Status) + }) + + t.Run("find non-existing condition", func(t *testing.T) { + condition := GetNodeCondition(node, v1.NodeDiskPressure) + assert.Nil(t, condition) + }) + + t.Run("nil conditions slice", func(t *testing.T) { + nodeNoConds := &v1.Node{Status: v1.NodeStatus{Conditions: nil}} + condition := GetNodeCondition(nodeNoConds, v1.NodeReady) + assert.Nil(t, condition) + }) + + t.Run("empty node status", func(t *testing.T) { + emptyNode := &v1.Node{} + condition := GetNodeCondition(emptyNode, v1.NodeReady) + assert.Nil(t, condition) + }) +} + +func TestIsMasterNode(t *testing.T) { + cases := []struct { + name string + node *v1.Node + isMaster bool + }{ + { + name: "master node", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{LabelNodeRoleMaster: "true"}}}, + isMaster: true, + }, + { + name: "worker node", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"node-role.kubernetes.io/worker": "true"}}}, + isMaster: false, + }, + { + name: "node without labels", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}}, + isMaster: false, + }, + { + name: "nil node labels", + node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: nil}}, + isMaster: false, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.isMaster, IsMasterNode(tt.node)) + }) + } +} + +func TestGetNodeInternalIP(t *testing.T) { + t.Run("node with internal IP", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "test-node"}, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + {Type: v1.NodeExternalIP, Address: "1.2.3.4"}, + }, + }, + } + + ip, err := GetNodeInternalIP(node) + assert.NoError(t, err) + assert.Equal(t, "192.168.1.100", ip) + }) + + t.Run("node without addresses", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "test-node"}, + Status: v1.NodeStatus{Addresses: []v1.NodeAddress{}}, + } + + _, err := GetNodeInternalIP(node) + assert.Error(t, err) + assert.Contains(t, err.Error(), "do not contains addresses") + }) + + t.Run("node without internal IP", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "test-node"}, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "1.2.3.4"}, + }, + }, + } + + _, err := GetNodeInternalIP(node) + assert.Error(t, err) + assert.Contains(t, err.Error(), "can not find InternalIP") + }) + + t.Run("node with IPv6 only", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "test-node"}, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "2001:db8::1"}, + }, + }, + } + + _, err := GetNodeInternalIP(node) + assert.Error(t, err) + assert.Contains(t, err.Error(), "can not find InternalIP") + }) +} + +func TestNodeInfo(t *testing.T) { + t.Run("normal node", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + ManagedFields: []metav1.ManagedFieldsEntry{ + {Manager: "test"}, + }, + }, + Status: v1.NodeStatus{ + Images: []v1.ContainerImage{ + {Names: []string{"image1"}}, + }, + }, + } + + info := NodeInfo(node) + assert.NotEmpty(t, info) + assert.Contains(t, info, "test-node") + assert.NotContains(t, info, "ManagedFields") + assert.NotContains(t, info, "Images") + }) + + t.Run("nil node", func(t *testing.T) { + info := NodeInfo(nil) + assert.Empty(t, info) + }) +} + +func TestFindNodeByNodeName(t *testing.T) { + nodes := []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node-1"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "node-2"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "node-3"}, + }, + } + + t.Run("find existing node", func(t *testing.T) { + node := FindNodeByNodeName(nodes, "node-2") + assert.NotNil(t, node) + assert.Equal(t, "node-2", node.Name) + }) + + t.Run("find first node", func(t *testing.T) { + node := FindNodeByNodeName(nodes, "node-1") + assert.NotNil(t, node) + assert.Equal(t, "node-1", node.Name) + }) + + t.Run("find last node", func(t *testing.T) { + node := FindNodeByNodeName(nodes, "node-3") + assert.NotNil(t, node) + assert.Equal(t, "node-3", node.Name) + }) + + t.Run("node not found", func(t *testing.T) { + node := FindNodeByNodeName(nodes, "node-4") + assert.Nil(t, node) + }) + + t.Run("empty node list", func(t *testing.T) { + node := FindNodeByNodeName([]v1.Node{}, "node-1") + assert.Nil(t, node) + }) + + t.Run("find in single node list", func(t *testing.T) { + singleNode := []v1.Node{ + {ObjectMeta: metav1.ObjectMeta{Name: "only-node"}}, + } + node := FindNodeByNodeName(singleNode, "only-node") + assert.NotNil(t, node) + assert.Equal(t, "only-node", node.Name) + }) +} diff --git a/pkg/controller/helper/pod_utils_test.go b/pkg/controller/helper/pod_utils_test.go new file mode 100644 index 000000000..93e85fbe5 --- /dev/null +++ b/pkg/controller/helper/pod_utils_test.go @@ -0,0 +1,361 @@ +package helper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestBuildReadinessGatePodConditionTypeWithPrefix(t *testing.T) { + prefix := "service.readiness.alibabacloud.com" + id := "test-svc" + expected := corev1.PodConditionType("service.readiness.alibabacloud.com/test-svc") + result := BuildReadinessGatePodConditionTypeWithPrefix(prefix, id) + assert.Equal(t, expected, result) +} + +func TestIsPodHasReadinessGate(t *testing.T) { + conditionType := "service.readiness.alibabacloud.com/test-svc" + t.Run("Pod has readiness gate", func(t *testing.T) { + pod := &corev1.Pod{ + Spec: corev1.PodSpec{ + ReadinessGates: []corev1.PodReadinessGate{ + {ConditionType: corev1.PodConditionType(conditionType)}, + }, + }, + } + result := IsPodHasReadinessGate(pod, conditionType) + assert.True(t, result) + }) + + t.Run("Pod does not have readiness gate", func(t *testing.T) { + pod := &corev1.Pod{ + Spec: corev1.PodSpec{ + ReadinessGates: []corev1.PodReadinessGate{}, + }, + } + result := IsPodHasReadinessGate(pod, conditionType) + assert.False(t, result) + }) + + t.Run("Pod has multiple readiness gates", func(t *testing.T) { + pod := &corev1.Pod{ + Spec: corev1.PodSpec{ + ReadinessGates: []corev1.PodReadinessGate{ + {ConditionType: "other-condition"}, + {ConditionType: corev1.PodConditionType(conditionType)}, + {ConditionType: "another-condition"}, + }, + }, + } + result := IsPodHasReadinessGate(pod, conditionType) + assert.True(t, result) + }) +} + +func TestIsPodContainersReady(t *testing.T) { + t.Run("Pod containers are ready", func(t *testing.T) { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.ContainersReady, + Status: corev1.ConditionTrue, + }, + }, + }, + } + result := IsPodContainersReady(pod) + assert.True(t, result) + }) + + t.Run("Pod containers are not ready", func(t *testing.T) { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.ContainersReady, + Status: corev1.ConditionFalse, + }, + }, + }, + } + result := IsPodContainersReady(pod) + assert.False(t, result) + }) + + t.Run("Pod has no ContainersReady condition", func(t *testing.T) { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }, + }, + }, + } + result := IsPodContainersReady(pod) + assert.False(t, result) + }) +} + +func TestGetPodCondition(t *testing.T) { + conditionType := corev1.PodReady + condition := corev1.PodCondition{ + Type: conditionType, + Status: corev1.ConditionTrue, + } + + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{condition}, + }, + } + + t.Run("Condition exists", func(t *testing.T) { + result := GetPodCondition(pod, conditionType) + assert.NotNil(t, result) + assert.Equal(t, condition, *result) + }) + + t.Run("Condition does not exist", func(t *testing.T) { + result := GetPodCondition(pod, corev1.ContainersReady) + assert.Nil(t, result) + }) +} + +func TestUpdatePodCondition(t *testing.T) { + conditionType := corev1.PodConditionType("service.readiness.alibabacloud.com/test-svc") + initialCondition := corev1.PodCondition{ + Type: conditionType, + Status: corev1.ConditionFalse, + } + newCondition := corev1.PodCondition{ + Type: conditionType, + Status: corev1.ConditionTrue, + Reason: ConditionReasonServerRegistered, + Message: ConditionMessageServerRegistered, + } + + t.Run("Add new condition", func(t *testing.T) { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{}, + }, + } + UpdatePodCondition(pod, newCondition) + assert.Len(t, pod.Status.Conditions, 1) + assert.Equal(t, newCondition, pod.Status.Conditions[0]) + }) + + t.Run("Update existing condition", func(t *testing.T) { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{initialCondition}, + }, + } + UpdatePodCondition(pod, newCondition) + assert.Len(t, pod.Status.Conditions, 1) + assert.Equal(t, newCondition, pod.Status.Conditions[0]) + }) +} + +func TestUpdateReadinessConditionForPod(t *testing.T) { + conditionType := corev1.PodConditionType("service.readiness.alibabacloud.com/test-svc") + reason := ConditionReasonServerRegistered + message := ConditionMessageServerRegistered + + t.Run("Pod without readiness gate", func(t *testing.T) { + pod := &corev1.Pod{} + client := fake.NewClientBuilder().Build() + err := UpdateReadinessConditionForPod(context.TODO(), client, pod, conditionType, corev1.ConditionTrue, reason, message) + assert.NoError(t, err) + }) + + t.Run("Pod with readiness gate but condition already exists with same values", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + ReadinessGates: []corev1.PodReadinessGate{ + {ConditionType: conditionType}, + }, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: conditionType, + Status: corev1.ConditionTrue, + Reason: reason, + Message: message, + }, + }, + }, + } + + client := fake.NewClientBuilder().WithObjects(pod).Build() + err := UpdateReadinessConditionForPod(context.TODO(), client, pod, conditionType, corev1.ConditionTrue, reason, message) + assert.NoError(t, err) + }) + + t.Run("Pod with readiness gate and condition needs update", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: types.UID("test-uid"), + }, + Spec: corev1.PodSpec{ + ReadinessGates: []corev1.PodReadinessGate{ + {ConditionType: conditionType}, + }, + }, + } + + client := fake.NewClientBuilder().WithObjects(pod).Build() + err := UpdateReadinessConditionForPod(context.TODO(), client, pod, conditionType, corev1.ConditionTrue, reason, message) + assert.NoError(t, err) + }) + + t.Run("Not found pod", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: types.UID("test-uid"), + }, + Spec: corev1.PodSpec{ + ReadinessGates: []corev1.PodReadinessGate{ + {ConditionType: conditionType}, + }, + }, + } + + c := fake.NewClientBuilder().Build() + err := c.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, &corev1.Pod{}) + assert.Error(t, err) + assert.True(t, errors.IsNotFound(err)) + + err = UpdateReadinessConditionForPod(context.TODO(), c, pod, conditionType, corev1.ConditionTrue, reason, message) + assert.NoError(t, err) + }) + + t.Run("Pod with same status but different reason", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-reason", + Namespace: "default", + UID: types.UID("test-uid-reason"), + }, + Spec: corev1.PodSpec{ + ReadinessGates: []corev1.PodReadinessGate{ + {ConditionType: conditionType}, + }, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: conditionType, + Status: corev1.ConditionTrue, + Reason: "OldReason", + Message: "old message", + }, + }, + }, + } + client := fake.NewClientBuilder().WithObjects(pod).Build() + err := UpdateReadinessConditionForPod(context.TODO(), client, pod, conditionType, corev1.ConditionTrue, reason, message) + assert.NoError(t, err) + }) +} + +func TestBuildPodConditionPatch(t *testing.T) { + t.Run("successfully create patch", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + UID: types.UID("test-uid-123"), + }, + } + condition := corev1.PodCondition{ + Type: corev1.PodConditionType("service.readiness.alibabacloud.com/test-svc"), + Status: corev1.ConditionTrue, + Reason: "TestReason", + Message: "TestMessage", + } + + patch, err := buildPodConditionPatch(pod, condition) + assert.NoError(t, err) + assert.NotNil(t, patch) + }) + + t.Run("create patch with different condition types", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-2", + Namespace: "default", + UID: types.UID("test-uid-456"), + }, + } + condition := corev1.PodCondition{ + Type: corev1.ContainersReady, + Status: corev1.ConditionFalse, + Reason: "ContainersNotReady", + Message: "Some containers are not ready", + } + + patch, err := buildPodConditionPatch(pod, condition) + assert.NoError(t, err) + assert.NotNil(t, patch) + }) + + t.Run("create patch with empty UID", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-3", + Namespace: "default", + UID: "", + }, + } + condition := corev1.PodCondition{ + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + } + + patch, err := buildPodConditionPatch(pod, condition) + assert.NoError(t, err) + assert.NotNil(t, patch) + }) + + t.Run("create patch with all condition fields", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-4", + Namespace: "default", + UID: types.UID("test-uid-789"), + }, + } + condition := corev1.PodCondition{ + Type: corev1.PodConditionType("custom.condition"), + Status: corev1.ConditionTrue, + LastProbeTime: metav1.Now(), + LastTransitionTime: metav1.Now(), + Reason: "CustomReason", + Message: "Custom message with details", + } + + patch, err := buildPodConditionPatch(pod, condition) + assert.NoError(t, err) + assert.NotNil(t, patch) + }) +} diff --git a/pkg/controller/helper/queue_test.go b/pkg/controller/helper/queue_test.go new file mode 100644 index 000000000..84893d5fe --- /dev/null +++ b/pkg/controller/helper/queue_test.go @@ -0,0 +1,520 @@ +package helper + +import ( + "errors" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" +) + +type mockObject struct { + v1.TypeMeta `json:",inline"` + v1.ObjectMeta `json:"metadata,omitempty"` + Name string + Namespace string +} + +func (m *mockObject) GetObjectKind() schema.ObjectKind { + return &m.TypeMeta +} + +func (m *mockObject) DeepCopyObject() runtime.Object { + return &mockObject{ + TypeMeta: m.TypeMeta, + ObjectMeta: m.ObjectMeta, + Name: m.Name, + Namespace: m.Namespace, + } +} + +func TestNewTaskQueue(t *testing.T) { + syncFn := func(obj interface{}) error { + return nil + } + + queue := NewTaskQueue(syncFn) + + assert.NotNil(t, queue) + assert.NotNil(t, queue.sync) + assert.NotNil(t, queue.queue) + assert.NotNil(t, queue.workerDone) + assert.NotNil(t, queue.fn) +} + +func TestNewCustomTaskQueue(t *testing.T) { + syncFn := func(obj interface{}) error { + return nil + } + + customKeyFunc := func(obj interface{}) (interface{}, error) { + return "custom-key", nil + } + + queue := NewCustomTaskQueue(syncFn, customKeyFunc) + + assert.NotNil(t, queue) + assert.NotNil(t, queue.fn) + + key, err := queue.fn("test-obj") + assert.NoError(t, err) + assert.Equal(t, "custom-key", key) +} + +func TestNewCustomTaskQueueWithNilKeyFunc(t *testing.T) { + syncFn := func(obj interface{}) error { + return nil + } + + queue := NewCustomTaskQueue(syncFn, nil) + + assert.NotNil(t, queue) + assert.NotNil(t, queue.fn) + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + key, err := queue.fn(mockObj) + assert.NoError(t, err) + assert.Equal(t, "default/test", key) +} + +func TestElementString(t *testing.T) { + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + element := Element{ + Key: mockObj, + Event: event, + } + + result := element.String() + assert.Equal(t, "default/test", result) +} + +func TestEnqueueTask(t *testing.T) { + var processedItems []interface{} + var mu sync.Mutex + + syncFn := func(obj interface{}) error { + mu.Lock() + defer mu.Unlock() + processedItems = append(processedItems, obj) + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + defer close(stopCh) + + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + queue.EnqueueTask(event) + + err := wait.PollImmediate(time.Millisecond*10, time.Second, func() (bool, error) { + mu.Lock() + defer mu.Unlock() + return len(processedItems) > 0, nil + }) + + assert.NoError(t, err) + + mu.Lock() + assert.Equal(t, 1, len(processedItems)) + mu.Unlock() +} + +func TestEnqueueSkippableTask(t *testing.T) { + var processedItems []interface{} + var mu sync.Mutex + + syncFn := func(obj interface{}) error { + mu.Lock() + defer mu.Unlock() + processedItems = append(processedItems, obj) + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + defer close(stopCh) + + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + queue.EnqueueSkippableTask(event) + + err := wait.PollImmediate(time.Millisecond*10, time.Second, func() (bool, error) { + mu.Lock() + defer mu.Unlock() + return len(processedItems) > 0, nil + }) + + assert.NoError(t, err) + + mu.Lock() + assert.Equal(t, 1, len(processedItems)) + mu.Unlock() +} + +func TestEnqueueTaskWhenShuttingDown(t *testing.T) { + syncFn := func(obj interface{}) error { + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + + // Start worker first + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + // Then shutdown + close(stopCh) + queue.Shutdown() + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + assert.NotPanics(t, func() { + queue.EnqueueTask(event) + }) +} + +func TestWorkerProcessing(t *testing.T) { + var processedItems []interface{} + var mu sync.Mutex + + syncFn := func(obj interface{}) error { + mu.Lock() + defer mu.Unlock() + processedItems = append(processedItems, obj) + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + defer close(stopCh) + + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + for i := 0; i < 3; i++ { + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: fmt.Sprintf("test-%d", i), + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + queue.EnqueueTask(event) + } + + err := wait.PollImmediate(time.Millisecond*10, time.Second*2, func() (bool, error) { + mu.Lock() + defer mu.Unlock() + return len(processedItems) == 3, nil + }) + + assert.NoError(t, err) + + mu.Lock() + assert.Equal(t, 3, len(processedItems)) + mu.Unlock() +} + +func TestWorkerErrorHandling(t *testing.T) { + var processedItems []interface{} + var mu sync.Mutex + processCount := 0 + + syncFn := func(obj interface{}) error { + mu.Lock() + defer mu.Unlock() + processCount++ + processedItems = append(processedItems, obj) + + if processCount <= 2 { + return errors.New("processing failed") + } + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + defer close(stopCh) + + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + queue.EnqueueTask(event) + + err := wait.PollImmediate(time.Millisecond*50, time.Second*5, func() (bool, error) { + mu.Lock() + defer mu.Unlock() + return processCount >= 3, nil + }) + + assert.NoError(t, err) + + mu.Lock() + assert.GreaterOrEqual(t, processCount, 3) + mu.Unlock() +} + +func TestIsShuttingDown(t *testing.T) { + syncFn := func(obj interface{}) error { + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + + assert.False(t, queue.IsShuttingDown()) + + // Start worker first + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + // Then shutdown + close(stopCh) + queue.Shutdown() + + assert.True(t, queue.IsShuttingDown()) +} + +func TestShutdown(t *testing.T) { + syncFn := func(obj interface{}) error { + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + queue.Shutdown() + + assert.True(t, queue.IsShuttingDown()) +} + +func TestDefaultKeyFunc(t *testing.T) { + queue := &Queue{} + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + key, err := queue.defaultKeyFunc(mockObj) + assert.NoError(t, err) + assert.Equal(t, "default/test", key) + + invalidObj := "invalid-object" + _, err = queue.defaultKeyFunc(invalidObj) + assert.Error(t, err) +} + +func TestEnqueueWithInvalidKey(t *testing.T) { + syncFn := func(obj interface{}) error { + return nil + } + + failingKeyFunc := func(obj interface{}) (interface{}, error) { + return nil, errors.New("key generation failed") + } + + queue := NewCustomTaskQueue(syncFn, failingKeyFunc) + stopCh := make(chan struct{}) + defer close(stopCh) + + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + assert.NotPanics(t, func() { + queue.EnqueueTask(event) + }) + + time.Sleep(time.Millisecond * 100) +} + +func TestLastSyncTimestamp(t *testing.T) { + syncFn := func(obj interface{}) error { + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + defer close(stopCh) + + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + initialLastSync := queue.lastSync + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + queue.EnqueueTask(event) + queue.Shutdown() + + assert.Greater(t, queue.lastSync, initialLastSync) +} + +func TestRateLimiting(t *testing.T) { + var processedItems []interface{} + var mu sync.Mutex + + syncFn := func(obj interface{}) error { + mu.Lock() + defer mu.Unlock() + processedItems = append(processedItems, obj) + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + defer close(stopCh) + + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + queue.EnqueueTask(event) + + err := wait.PollImmediate(time.Millisecond*10, time.Second, func() (bool, error) { + mu.Lock() + defer mu.Unlock() + return len(processedItems) > 0, nil + }) + + assert.NoError(t, err) + assert.NotNil(t, queue.queue) +} + +func BenchmarkEnqueueTask(b *testing.B) { + syncFn := func(obj interface{}) error { + return nil + } + + queue := NewTaskQueue(syncFn) + stopCh := make(chan struct{}) + + // Start worker + go queue.Run(1, time.Millisecond*10, stopCh) + time.Sleep(time.Millisecond * 50) + + mockObj := &mockObject{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + event := Event{ + Type: CreateEvent, + Obj: mockObj, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + queue.EnqueueTask(event) + } + b.StopTimer() + + // Cleanup + close(stopCh) + queue.Shutdown() +} diff --git a/pkg/controller/helper/reference_indexer_test.go b/pkg/controller/helper/reference_indexer_test.go new file mode 100644 index 000000000..dd0776ead --- /dev/null +++ b/pkg/controller/helper/reference_indexer_test.go @@ -0,0 +1,182 @@ +package helper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + networking "k8s.io/api/networking/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNewDefaultReferenceIndexer(t *testing.T) { + indexer := NewDefaultReferenceIndexer() + assert.NotNil(t, indexer) + assert.IsType(t, &defaultReferenceIndexer{}, indexer) +} + +func TestDefaultReferenceIndexer_BuildServiceRefIndexes_EmptyIngress(t *testing.T) { + indexer := NewDefaultReferenceIndexer() + ingress := &networking.Ingress{} + + result := indexer.BuildServiceRefIndexes(context.Background(), ingress) + assert.Empty(t, result) +} + +func TestDefaultReferenceIndexer_BuildServiceRefIndexes_OnlyDefaultBackend(t *testing.T) { + indexer := NewDefaultReferenceIndexer() + ingress := &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-ingress", + Namespace: "default", + }, + Spec: networking.IngressSpec{ + DefaultBackend: &networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "default-service", + }, + }, + }, + } + + result := indexer.BuildServiceRefIndexes(context.Background(), ingress) + assert.Len(t, result, 1) + assert.Contains(t, result, "default-service") +} + +func TestDefaultReferenceIndexer_BuildServiceRefIndexes_OnlyHTTPPaths(t *testing.T) { + indexer := NewDefaultReferenceIndexer() + ingress := &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-ingress", + Namespace: "default", + }, + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Backend: networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "service1", + }, + }, + }, + { + Backend: networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "service2", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + result := indexer.BuildServiceRefIndexes(context.Background(), ingress) + assert.Len(t, result, 2) + assert.Contains(t, result, "service1") + assert.Contains(t, result, "service2") +} + +func TestDefaultReferenceIndexer_BuildServiceRefIndexes_MixedBackends(t *testing.T) { + indexer := NewDefaultReferenceIndexer() + ingress := &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-ingress", + Namespace: "default", + }, + Spec: networking.IngressSpec{ + DefaultBackend: &networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "default-service", + }, + }, + Rules: []networking.IngressRule{ + { + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Backend: networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "path-service1", + }, + }, + }, + { + Backend: networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "path-service2", + }, + }, + }, + }, + }, + }, + }, + { + // Rule without HTTP + }, + }, + }, + } + + result := indexer.BuildServiceRefIndexes(context.Background(), ingress) + assert.Len(t, result, 3) + assert.Contains(t, result, "default-service") + assert.Contains(t, result, "path-service1") + assert.Contains(t, result, "path-service2") +} + +func TestDefaultReferenceIndexer_BuildServiceRefIndexes_DuplicateServices(t *testing.T) { + indexer := NewDefaultReferenceIndexer() + ingress := &networking.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-ingress", + Namespace: "default", + }, + Spec: networking.IngressSpec{ + DefaultBackend: &networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "duplicate-service", + }, + }, + Rules: []networking.IngressRule{ + { + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Backend: networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "duplicate-service", + }, + }, + }, + { + Backend: networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "other-service", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + result := indexer.BuildServiceRefIndexes(context.Background(), ingress) + assert.Len(t, result, 2) // Should deduplicate + assert.Contains(t, result, "duplicate-service") + assert.Contains(t, result, "other-service") +} diff --git a/pkg/controller/helper/service_utils_test.go b/pkg/controller/helper/service_utils_test.go index f4fe56887..55977a6d3 100644 --- a/pkg/controller/helper/service_utils_test.go +++ b/pkg/controller/helper/service_utils_test.go @@ -1,45 +1,20 @@ package helper import ( + "fmt" + "os" "testing" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/util/feature" + ctrlCfg "k8s.io/cloud-provider-alibaba-cloud/pkg/config" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model" ) -func TestIsServiceHashChanged(t *testing.T) { - base := getDefaultService() - base.Annotations["service.beta.kubernetes.io/alibaba-cloud-loadbalancer-name"] = "slb-base" - baseHash := GetServiceHash(base) - - svcAnnoChanged := base.DeepCopy() - svcAnnoChanged.Annotations["service.beta.kubernetes.io/alibaba-cloud-loadbalancer-name"] = "slb-anno-changed" - annoHash := GetServiceHash(svcAnnoChanged) - assert.NotEqual(t, baseHash, annoHash) - - svcMetaChanged := base.DeepCopy() - svcMetaChanged.Labels = map[string]string{"app": "test"} - hash := GetServiceHash(svcMetaChanged) - assert.Equal(t, baseHash, hash) - - svcSpecChanged := base.DeepCopy() - svcSpecChanged.Spec.ExternalTrafficPolicy = "Cluster" - hash = GetServiceHash(svcSpecChanged) - assert.NotEqual(t, baseHash, hash) - - svcNewAttrChanged := base.DeepCopy() - svcNewAttrChanged.Spec.PublishNotReadyAddresses = true - hash = GetServiceHash(svcNewAttrChanged) - assert.Equal(t, baseHash, hash) - - svcLoadBalancerSourceRangesAdded := base.DeepCopy() - svcLoadBalancerSourceRangesAdded.Spec.LoadBalancerSourceRanges = []string{"10.0.0.0/8"} - hash = GetServiceHash(svcLoadBalancerSourceRangesAdded) - assert.NotEqual(t, baseHash, hash) -} - func getDefaultService() *v1.Service { return &v1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -63,15 +38,454 @@ func getDefaultService() *v1.Service { } } +func TestIsServiceHashChanged(t *testing.T) { + base := getDefaultService() + base.Annotations["service.beta.kubernetes.io/alibaba-cloud-loadbalancer-name"] = "slb-base" + baseHash := GetServiceHash(base) + base.Labels = map[string]string{ + LabelServiceHash: baseHash, + } + + t.Run("empty service hash label", func(t *testing.T) { + svcEmptyLabel := base.DeepCopy() + svcEmptyLabel.Labels = map[string]string{} + assert.True(t, IsServiceHashChanged(svcEmptyLabel)) + }) + t.Run("service annotation changed", func(t *testing.T) { + svcAnnoChanged := base.DeepCopy() + svcAnnoChanged.Annotations["service.beta.kubernetes.io/alibaba-cloud-loadbalancer-name"] = "slb-anno-changed" + assert.True(t, IsServiceHashChanged(svcAnnoChanged)) + }) + + t.Run("service label changed", func(t *testing.T) { + svcLabelChanged := base.DeepCopy() + svcLabelChanged.Labels["app"] = "test" + assert.False(t, IsServiceHashChanged(svcLabelChanged)) + }) + + t.Run("external traffic policy changed", func(t *testing.T) { + svcSpecChanged := base.DeepCopy() + svcSpecChanged.Spec.ExternalTrafficPolicy = "Cluster" + assert.True(t, IsServiceHashChanged(svcSpecChanged)) + }) + + t.Run("unimportant spec fields changed", func(t *testing.T) { + svcNewAttrChanged := base.DeepCopy() + svcNewAttrChanged.Spec.PublishNotReadyAddresses = true + assert.False(t, IsServiceHashChanged(svcNewAttrChanged)) + }) +} + +func TestIsENIBackendType(t *testing.T) { + oldEnv := os.Getenv("SERVICE_FORCE_BACKEND_ENI") + oldServiceBackendType := ctrlCfg.CloudCFG.Global.ServiceBackendType + defer func() { + err := os.Setenv("SERVICE_FORCE_BACKEND_ENI", oldEnv) + assert.NoError(t, err) + ctrlCfg.CloudCFG.Global.ServiceBackendType = oldServiceBackendType + }() + + err := os.Setenv("SERVICE_FORCE_BACKEND_ENI", "") + assert.NoError(t, err) + ctrlCfg.CloudCFG.Global.ServiceBackendType = model.ECSBackendType + + t.Run("not eni backend type", func(t *testing.T) { + svc := getDefaultService() + assert.False(t, IsENIBackendType(svc)) + }) + + t.Run("backend type from service annotations", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations = map[string]string{ + BackendType: model.ENIBackendType, + } + assert.True(t, IsENIBackendType(svc)) + }) + + t.Run("force set eni backend type from environment variable", func(t *testing.T) { + err := os.Setenv("SERVICE_FORCE_BACKEND_ENI", "true") + assert.NoError(t, err) + svc := getDefaultService() + assert.True(t, IsENIBackendType(svc)) + err = os.Setenv("SERVICE_FORCE_BACKEND_ENI", "") + assert.NoError(t, err) + }) + + t.Run("eni backend type from cloud config", func(t *testing.T) { + ctrlCfg.CloudCFG.Global.ServiceBackendType = model.ENIBackendType + svc := getDefaultService() + assert.True(t, IsENIBackendType(svc)) + ctrlCfg.CloudCFG.Global.ServiceBackendType = model.ECSBackendType + }) +} + func TestIsTunnelTypeService(t *testing.T) { - svc := getDefaultService() - svc.Annotations = map[string]string{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-tunnel-type": "tunnel", + t.Run("service with tunnel type annotation", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations = map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-tunnel-type": "tunnel", + } + assert.True(t, IsTunnelTypeService(svc)) + }) + + t.Run("service with empty tunnel type annotation", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations = map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-tunnel-type": "", + } + assert.True(t, IsTunnelTypeService(svc)) + }) + + t.Run("service without tunnel type annotation", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations = map[string]string{ + "other-annotation": "value", + } + assert.False(t, IsTunnelTypeService(svc)) + }) + + t.Run("service with nil annotations", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations = nil + assert.False(t, IsTunnelTypeService(svc)) + }) +} + +func TestRetry(t *testing.T) { + t.Run("success on first attempt", func(t *testing.T) { + callCount := 0 + err := Retry(nil, func(svc *v1.Service) error { + callCount++ + return nil + }, nil) + + assert.NoError(t, err) + assert.Equal(t, 1, callCount) + }) + + t.Run("retry until success", func(t *testing.T) { + callCount := 0 + err := Retry(&wait.Backoff{Steps: 3}, func(svc *v1.Service) error { + callCount++ + if callCount < 2 { + return fmt.Errorf("please try again") + } + return nil + }, nil) + + assert.NoError(t, err) + assert.Equal(t, 2, callCount) + }) + + t.Run("retry until max attempts", func(t *testing.T) { + callCount := 0 + err := Retry(&wait.Backoff{Steps: 3}, func(svc *v1.Service) error { + callCount++ + return fmt.Errorf("please try again") + }, nil) + + assert.Error(t, err) // ExponentialBackoff returns nil when it exhausts steps + assert.Equal(t, 3, callCount) + }) + + t.Run("non-retryable error", func(t *testing.T) { + callCount := 0 + err := Retry(&wait.Backoff{Steps: 3}, func(svc *v1.Service) error { + callCount++ + return fmt.Errorf("permanent error") + }, nil) + + assert.NoError(t, err) // ExponentialBackoff returns nil even for non-retryable errors + assert.Equal(t, 1, callCount) // Should not retry + }) +} + +func TestNeedCLB(t *testing.T) { + t.Run("no load balancer type", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.Type = v1.ServiceTypeClusterIP + assert.False(t, NeedCLB(svc)) + }) + + t.Run("has load balancer class", func(t *testing.T) { + svc := getDefaultService() + class := "some-class" + svc.Spec.LoadBalancerClass = &class + assert.False(t, NeedCLB(svc)) + }) + + t.Run("has tunnel type annotation", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[TunnelType] = "tunnel" + assert.False(t, NeedCLB(svc)) + }) + + t.Run("has load balancer class annotation", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[LoadBalancerClass] = "class" + assert.False(t, NeedCLB(svc)) + }) + + t.Run("has LoadBalancerType annotation with feature gate enabled", func(t *testing.T) { + oldValue := feature.DefaultMutableFeatureGate.Enabled(ctrlCfg.LoadBalancerTypeAnnotation) + feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + ctrlCfg.LoadBalancerTypeAnnotation: true, + }) + defer func() { + feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + ctrlCfg.LoadBalancerTypeAnnotation: oldValue, + }) + }() + svc := getDefaultService() + svc.Annotations[LoadBalancerType] = "clb" + assert.False(t, NeedCLB(svc)) + }) + + t.Run("default case - need CLB", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations = map[string]string{} + assert.True(t, NeedCLB(svc)) + }) +} + +func TestNeedNLB(t *testing.T) { + t.Run("not load balancer type", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.Type = v1.ServiceTypeClusterIP + assert.False(t, NeedNLB(svc)) + }) + + t.Run("has NLB load balancer class", func(t *testing.T) { + svc := getDefaultService() + class := NLBClass + svc.Spec.LoadBalancerClass = &class + assert.True(t, NeedNLB(svc)) + }) + + t.Run("has NLB type annotation without feature gate enabled", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[LoadBalancerType] = "nlb" + assert.False(t, NeedNLB(svc)) + }) + + t.Run("has NLB type annotation with feature gate enabled", func(t *testing.T) { + oldValue := feature.DefaultMutableFeatureGate.Enabled(ctrlCfg.LoadBalancerTypeAnnotation) + feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + ctrlCfg.LoadBalancerTypeAnnotation: true, + }) + defer func() { + feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + ctrlCfg.LoadBalancerTypeAnnotation: oldValue, + }) + }() + svc := getDefaultService() + svc.Annotations[LoadBalancerType] = "nlb" + assert.True(t, NeedNLB(svc)) + }) + + t.Run("non NLB load balancer class", func(t *testing.T) { + svc := getDefaultService() + class := "other-class" + svc.Spec.LoadBalancerClass = &class + assert.False(t, NeedNLB(svc)) + }) + + t.Run("non NLB type annotation", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[LoadBalancerType] = "clb" + assert.False(t, NeedNLB(svc)) + }) +} + +func TestGetServiceTrafficPolicy(t *testing.T) { + t.Run("ENI backend type", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[BackendType] = model.ENIBackendType + policy, err := GetServiceTrafficPolicy(svc) + assert.NoError(t, err) + assert.Equal(t, ENITrafficPolicy, policy) + }) + + t.Run("ClusterIP service", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.Type = v1.ServiceTypeClusterIP + _, err := GetServiceTrafficPolicy(svc) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cluster service type just support eni mode") + }) + + t.Run("Local traffic policy", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeLocal + policy, err := GetServiceTrafficPolicy(svc) + assert.NoError(t, err) + assert.Equal(t, LocalTrafficPolicy, policy) + }) + + t.Run("Cluster traffic policy", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeCluster + policy, err := GetServiceTrafficPolicy(svc) + assert.NoError(t, err) + assert.Equal(t, ClusterTrafficPolicy, policy) + }) +} + +func TestIsLocalModeService(t *testing.T) { + t.Run("local mode", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeLocal + assert.True(t, IsLocalModeService(svc)) + }) + + t.Run("cluster mode", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeCluster + assert.False(t, IsLocalModeService(svc)) + }) +} + +func TestIsClusterIPService(t *testing.T) { + t.Run("ClusterIP service", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.Type = v1.ServiceTypeClusterIP + assert.True(t, IsClusterIPService(svc)) + }) + + t.Run("LoadBalancer service", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.Type = v1.ServiceTypeLoadBalancer + assert.False(t, IsClusterIPService(svc)) + }) + + t.Run("NodePort service", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.Type = v1.ServiceTypeNodePort + assert.False(t, IsClusterIPService(svc)) + }) +} + +func TestNeedDeleteLoadBalancer(t *testing.T) { + now := metav1.Now() + + t.Run("service with deletion timestamp", func(t *testing.T) { + svc := getDefaultService() + svc.DeletionTimestamp = &now + assert.True(t, NeedDeleteLoadBalancer(svc)) + }) + + t.Run("service type changed to ClusterIP", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.Type = v1.ServiceTypeClusterIP + assert.True(t, NeedDeleteLoadBalancer(svc)) + }) + + t.Run("service type changed to NodePort", func(t *testing.T) { + svc := getDefaultService() + svc.Spec.Type = v1.ServiceTypeNodePort + assert.True(t, NeedDeleteLoadBalancer(svc)) + }) + + t.Run("normal LoadBalancer service", func(t *testing.T) { + svc := getDefaultService() + assert.False(t, NeedDeleteLoadBalancer(svc)) + }) +} + +func TestRetryOnErrorContains(t *testing.T) { + t.Run("success on first attempt", func(t *testing.T) { + callCount := 0 + err := RetryOnErrorContains("retry", func() error { + callCount++ + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 1, callCount) + }) + + t.Run("retry on matching error", func(t *testing.T) { + callCount := 0 + err := RetryOnErrorContains("retry", func() error { + callCount++ + if callCount < 3 { + return fmt.Errorf("please retry this operation") + } + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 3, callCount) + }) + + t.Run("no retry on non-matching error", func(t *testing.T) { + callCount := 0 + err := RetryOnErrorContains("retry", func() error { + callCount++ + return fmt.Errorf("permanent error") + }) + assert.Error(t, err) + assert.Equal(t, 1, callCount) + }) +} + +func TestIs7LayerProtocol(t *testing.T) { + tests := []struct { + protocol string + want bool + }{ + {model.HTTP, true}, + {model.HTTPS, true}, + {model.TCP, false}, + {model.UDP, false}, + {"grpc", false}, } - assert.Equal(t, true, IsTunnelTypeService(svc)) - svc.Annotations = map[string]string{ - "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-tunnel-type": "", + for _, tt := range tests { + t.Run(tt.protocol, func(t *testing.T) { + assert.Equal(t, tt.want, Is7LayerProtocol(tt.protocol)) + }) } - assert.Equal(t, true, IsTunnelTypeService(svc)) +} + +func TestIs4LayerProtocol(t *testing.T) { + tests := []struct { + protocol string + want bool + }{ + {model.TCP, true}, + {model.UDP, true}, + {model.HTTP, false}, + {model.HTTPS, false}, + {"grpc", false}, + } + + for _, tt := range tests { + t.Run(tt.protocol, func(t *testing.T) { + assert.Equal(t, tt.want, Is4LayerProtocol(tt.protocol)) + }) + } +} + +func TestIsServiceOwnIngress(t *testing.T) { + t.Run("nil service", func(t *testing.T) { + assert.False(t, IsServiceOwnIngress(nil)) + }) + + t.Run("service without ingress", func(t *testing.T) { + svc := getDefaultService() + assert.False(t, IsServiceOwnIngress(svc)) + }) + + t.Run("service with empty ingress list", func(t *testing.T) { + svc := getDefaultService() + svc.Status.LoadBalancer.Ingress = []v1.LoadBalancerIngress{} + assert.False(t, IsServiceOwnIngress(svc)) + }) + + t.Run("service with ingress", func(t *testing.T) { + svc := getDefaultService() + svc.Status.LoadBalancer.Ingress = []v1.LoadBalancerIngress{ + {IP: "1.2.3.4"}, + } + assert.True(t, IsServiceOwnIngress(svc)) + }) } diff --git a/pkg/controller/helper/tags_test.go b/pkg/controller/helper/tags_test.go index dfe2624b2..23784ff58 100644 --- a/pkg/controller/helper/tags_test.go +++ b/pkg/controller/helper/tags_test.go @@ -68,3 +68,99 @@ func TestFilterRemoteTags(t *testing.T) { }) } } + +func TestDiffLoadBalancerTags(t *testing.T) { + tests := []struct { + name string + local []tag.Tag + remote []tag.Tag + wantTag []tag.Tag + wantUntag []tag.Tag + }{ + { + name: "add new tags", + local: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + remote: []tag.Tag{}, + wantTag: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + wantUntag: []tag.Tag{}, + }, + { + name: "remove tags", + local: []tag.Tag{}, + remote: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + wantTag: []tag.Tag{}, + wantUntag: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + }, + { + name: "update existing tags", + local: []tag.Tag{ + {Key: "key1", Value: "value1-new"}, + {Key: "key2", Value: "value2"}, + }, + remote: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + {Key: "key3", Value: "value3"}, + }, + wantTag: []tag.Tag{ + {Key: "key1", Value: "value1-new"}, + }, + wantUntag: []tag.Tag{ + {Key: "key3", Value: "value3"}, + }, + }, + { + name: "no changes", + local: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + remote: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + wantTag: []tag.Tag{}, + wantUntag: []tag.Tag{}, + }, + { + name: "mixed operations", + local: []tag.Tag{ + {Key: "key1", Value: "value1"}, // unchanged + {Key: "key2", Value: "value2-new"}, // updated + {Key: "key4", Value: "value4"}, // added + }, + remote: []tag.Tag{ + {Key: "key1", Value: "value1"}, // unchanged + {Key: "key2", Value: "value2"}, // updated + {Key: "key3", Value: "value3"}, // removed + }, + wantTag: []tag.Tag{ + {Key: "key2", Value: "value2-new"}, + {Key: "key4", Value: "value4"}, + }, + wantUntag: []tag.Tag{ + {Key: "key3", Value: "value3"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotTag, gotUntag := DiffLoadBalancerTags(tt.local, tt.remote) + assert.ElementsMatch(t, tt.wantTag, gotTag, "tag operations mismatch") + assert.ElementsMatch(t, tt.wantUntag, gotUntag, "untag operations mismatch") + }) + } +} diff --git a/pkg/controller/node/event_handler_test.go b/pkg/controller/node/event_handler_test.go new file mode 100644 index 000000000..b0c3533e3 --- /dev/null +++ b/pkg/controller/node/event_handler_test.go @@ -0,0 +1,167 @@ +package node + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +func TestEnqueueRequestForNodeEvent_Update(t *testing.T) { + handler := NewEnqueueRequestForNodeEvent() + assert.NotNil(t, handler) + + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + defer queue.ShutDown() + + oldNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + } + + newNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + } + + updateEvent := event.UpdateEvent{ + ObjectOld: oldNode, + ObjectNew: newNode, + } + + // Call Update - should not add anything to queue (empty implementation) + handler.Update(context.Background(), updateEvent, queue) + + // Verify queue is empty since Update does nothing + assert.Equal(t, 0, queue.Len(), "Update should not add items to queue") +} + +func TestEnqueueRequestForNodeEvent_Delete(t *testing.T) { + handler := NewEnqueueRequestForNodeEvent() + assert.NotNil(t, handler) + + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + defer queue.ShutDown() + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + } + + deleteEvent := event.DeleteEvent{ + Object: node, + } + + // Call Delete - should not add anything to queue (empty implementation) + handler.Delete(context.Background(), deleteEvent, queue) + + // Verify queue is empty since Delete does nothing + assert.Equal(t, 0, queue.Len(), "Delete should not add items to queue") +} + +func TestEnqueueRequestForNodeEvent_Generic(t *testing.T) { + handler := NewEnqueueRequestForNodeEvent() + assert.NotNil(t, handler) + + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + defer queue.ShutDown() + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + } + + genericEvent := event.GenericEvent{ + Object: node, + } + + // Call Generic - should not add anything to queue (empty implementation) + handler.Generic(context.Background(), genericEvent, queue) + + // Verify queue is empty since Generic does nothing + assert.Equal(t, 0, queue.Len(), "Generic should not add items to queue") +} + +func TestEnqueueRequestForNodeEvent_Create(t *testing.T) { + tests := []struct { + name string + node *v1.Node + expectedQueue int + description string + }{ + { + name: "node with cloud taint should be enqueued", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-1", + }, + Spec: v1.NodeSpec{ + Taints: []v1.Taint{ + { + Key: "node.cloudprovider.kubernetes.io/uninitialized", + Value: "true", + Effect: v1.TaintEffectNoSchedule, + }, + }, + }, + }, + expectedQueue: 1, + description: "should enqueue node with cloud taint", + }, + { + name: "node without cloud taint should not be enqueued", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-2", + }, + Spec: v1.NodeSpec{ + Taints: []v1.Taint{ + { + Key: "some-other-taint", + Value: "true", + Effect: v1.TaintEffectNoSchedule, + }, + }, + }, + }, + expectedQueue: 0, + description: "should not enqueue node without cloud taint", + }, + { + name: "node with no taints should not be enqueued", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-3", + }, + Spec: v1.NodeSpec{ + Taints: []v1.Taint{}, + }, + }, + expectedQueue: 0, + description: "should not enqueue node with no taints", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := NewEnqueueRequestForNodeEvent() + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + defer queue.ShutDown() + + createEvent := event.CreateEvent{ + Object: tt.node, + } + + handler.Create(context.Background(), createEvent, queue) + assert.Equal(t, tt.expectedQueue, queue.Len(), tt.description) + }) + } +} diff --git a/pkg/controller/node/manager.go b/pkg/controller/node/manager.go index 16d1feb6d..5a30d1cb6 100644 --- a/pkg/controller/node/manager.go +++ b/pkg/controller/node/manager.go @@ -261,7 +261,7 @@ func setFields(node *v1.Node, ins *prvd.NodeAttribute, cfgRoute bool, removeTain if ins.Region != "" { klog.V(5).Infof( - "node %s,Adding node label from cloud provider: %s=%s, %s=%s", + "node %s, Adding node label from cloud provider: %s=%s, %s=%s", node.Name, v1.LabelZoneRegion, ins.Region, v1.LabelTopologyRegion, ins.Region, @@ -276,7 +276,7 @@ func setFields(node *v1.Node, ins *prvd.NodeAttribute, cfgRoute bool, removeTain if node.Spec.ProviderID == "" && ins.InstanceID != "" { prvdId := fmt.Sprintf("%s.%s", ins.Region, ins.InstanceID) klog.V(5).Infof( - "node %s,Adding provider id from cloud provider: %s", + "node %s, Adding provider id from cloud provider: %s", node.Name, prvdId, ) diff --git a/pkg/controller/node/manager_test.go b/pkg/controller/node/manager_test.go index 31e82920a..6a6ef8fba 100644 --- a/pkg/controller/node/manager_test.go +++ b/pkg/controller/node/manager_test.go @@ -1,12 +1,15 @@ package node import ( + "errors" "testing" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" + "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" + "k8s.io/cloud-provider/api" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -134,3 +137,532 @@ func TestGetNodeType(t *testing.T) { }) } } + +func TestSetNetworkUnavailable(t *testing.T) { + t.Run("node without NodeNetworkUnavailable condition", func(t *testing.T) { + // Test case 1: Node without NodeNetworkUnavailable condition + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{}, + }, + } + + setNetworkUnavailable(node) + + condition := getNodeCondition(node, v1.NodeNetworkUnavailable) + assert.NotNil(t, condition) + assert.Equal(t, v1.ConditionTrue, condition.Status) + assert.Equal(t, "NoRouteCreated", condition.Reason) + }) + + t.Run("node with NodeNetworkUnavailable already set to False", func(t *testing.T) { + // Test case 2: Node with NodeNetworkUnavailable already set to False + node2 := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-2", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeNetworkUnavailable, + Status: v1.ConditionFalse, + }, + }, + }, + } + + setNetworkUnavailable(node2) + + condition2 := getNodeCondition(node2, v1.NodeNetworkUnavailable) + assert.NotNil(t, condition2) + assert.Equal(t, v1.ConditionFalse, condition2.Status) + }) +} + +func TestRemoveCloudTaints(t *testing.T) { + // Test case 1: Node with cloud taint + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + Spec: v1.NodeSpec{ + Taints: []v1.Taint{ + { + Key: "node.cloudprovider.kubernetes.io/uninitialized", + Value: "true", + Effect: v1.TaintEffectNoSchedule, + }, + { + Key: "other-taint", + Value: "value", + Effect: v1.TaintEffectNoExecute, + }, + }, + }, + } + + removeCloudTaints(node) + + assert.Equal(t, 1, len(node.Spec.Taints)) + assert.Equal(t, "other-taint", node.Spec.Taints[0].Key) + + // Test case 2: Node without cloud taint + node2 := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-2", + }, + Spec: v1.NodeSpec{ + Taints: []v1.Taint{ + { + Key: "other-taint", + Value: "value", + Effect: v1.TaintEffectNoExecute, + }, + }, + }, + } + + removeCloudTaints(node2) + + assert.Equal(t, 1, len(node2.Spec.Taints)) + assert.Equal(t, "other-taint", node2.Spec.Taints[0].Key) +} + +func TestFindCloudTaint(t *testing.T) { + tests := []struct { + name string + taints []v1.Taint + expected *v1.Taint + }{ + { + name: "has cloud taint", + taints: []v1.Taint{ + {Key: v1.TaintNodeNetworkUnavailable, Value: "test", Effect: v1.TaintEffectNoSchedule}, + {Key: api.TaintExternalCloudProvider, Value: "true", Effect: v1.TaintEffectNoSchedule}, + }, + expected: &v1.Taint{Key: api.TaintExternalCloudProvider, Value: "true", Effect: v1.TaintEffectNoSchedule}, + }, + { + name: "no cloud taint", + taints: []v1.Taint{ + {Key: v1.TaintNodeNetworkUnavailable, Value: "test", Effect: v1.TaintEffectNoSchedule}, + }, + expected: nil, + }, + { + name: "empty taints", + taints: []v1.Taint{}, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := findCloudTaint(tt.taints) + if tt.expected == nil { + assert.Nil(t, result) + } else { + assert.NotNil(t, result) + assert.Equal(t, tt.expected.Key, result.Key) + assert.Equal(t, tt.expected.Value, result.Value) + assert.Equal(t, tt.expected.Effect, result.Effect) + } + }) + } +} + +func TestExcludeTaintFromList(t *testing.T) { + taints := []v1.Taint{ + { + Key: "node.cloudprovider.kubernetes.io/uninitialized", + Value: "true", + Effect: v1.TaintEffectNoSchedule, + }, + { + Key: "other-taint", + Value: "value", + Effect: v1.TaintEffectNoExecute, + }, + } + + toExclude := v1.Taint{ + Key: "node.cloudprovider.kubernetes.io/uninitialized", + Value: "true", + Effect: v1.TaintEffectNoSchedule, + } + + result := excludeTaintFromList(taints, toExclude) + assert.Equal(t, 1, len(result)) + assert.Equal(t, "other-taint", result[0].Key) +} + +func TestFindCloudECSById(t *testing.T) { + mockECS := &vmock.MockECS{} + nodeWithProviderID := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-1", + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.i-123456", + }, + } + + ins, err := findCloudECSById(mockECS, nodeWithProviderID) + assert.NoError(t, err) + assert.NotNil(t, ins) + assert.Equal(t, "i-123456", ins.InstanceID) + assert.Equal(t, vmock.InstanceType, ins.InstanceType) + assert.Equal(t, vmock.ZoneID, ins.Zone) + assert.Equal(t, vmock.RegionID, ins.Region) +} + +func TestFindCloudECSById_ListInstancesError(t *testing.T) { + mockECS := &vmock.MockECS{} + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "test-node"}, + Spec: v1.NodeSpec{ProviderID: "cn-hangzhou.ecs-list-instances-error"}, + } + ins, err := findCloudECSById(mockECS, node) + assert.Error(t, err) + assert.Nil(t, ins) + assert.Contains(t, err.Error(), "cloud instance api fail") +} + +func TestFindCloudECSById_NotFound(t *testing.T) { + mockECS := &vmock.MockECS{} + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "test-node"}, + Spec: v1.NodeSpec{ProviderID: "cn-hangzhou.not-exists"}, + } + ins, err := findCloudECSById(mockECS, node) + assert.Error(t, err) + assert.Equal(t, ErrNotFound, err) + assert.Nil(t, ins) +} + +func TestFindCloudECSByIp(t *testing.T) { + mockECS := &vmock.MockECS{} + t.Run("node with single ip", func(t *testing.T) { + nodeWithSingleIP := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "192.168.1.100", + }, + }, + }, + } + + ins, err := findCloudECSByIp(mockECS, nodeWithSingleIP) + assert.Error(t, err) + assert.Equal(t, ErrNotFound, err) + assert.Nil(t, ins) + }) + + t.Run("node without ip", func(t *testing.T) { + nodeWithoutIP := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-2", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{}, + }, + } + + ins, err := findCloudECSByIp(mockECS, nodeWithoutIP) + assert.Error(t, err) + assert.Equal(t, ErrNotFound, err) + assert.Nil(t, ins) + }) + + t.Run("node with multiple ips", func(t *testing.T) { + nodeWithMultipleIPs := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-3", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "192.168.1.100", + }, + { + Type: v1.NodeInternalIP, + Address: "192.168.1.101", + }, + }, + }, + } + + ins, err := findCloudECSByIp(mockECS, nodeWithMultipleIPs) + assert.Error(t, err) + assert.Equal(t, ErrNotFound, err) + assert.Nil(t, ins) + }) +} + +func TestFindCloudECS(t *testing.T) { + mockECS := &vmock.MockECS{} + t.Run("node with provider id", func(t *testing.T) { + nodeWithProviderID := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-1", + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.i-123456", + }, + } + + ins, err := findCloudECS(mockECS, nodeWithProviderID) + assert.NoError(t, err) + assert.NotNil(t, ins) + }) + + t.Run("node without provider id", func(t *testing.T) { + nodeWithoutProviderID := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-2", + }, + Spec: v1.NodeSpec{ + ProviderID: "", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "192.168.1.100", + }, + }, + }, + } + + ins, err := findCloudECS(mockECS, nodeWithoutProviderID) + assert.Error(t, err) + assert.Equal(t, ErrNotFound, err) + assert.Nil(t, ins) + }) + +} + +func TestBatchOperate(t *testing.T) { + t.Run("normal case with less than MAX_BATCH_NUM nodes", func(t *testing.T) { + nodes := make([]v1.Node, 50) + for i := 0; i < 50; i++ { + nodes[i] = v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-" + string(rune(i)), + Labels: map[string]string{}, + }, + } + } + + count := 0 + batchFunc := func(batch []v1.Node, last bool) error { + count += len(batch) + return nil + } + + err := batchOperate(nodes, batchFunc) + assert.NoError(t, err) + assert.Equal(t, 50, count) + }) + + t.Run("batch case with more than MAX_BATCH_NUM nodes", func(t *testing.T) { + nodes := make([]v1.Node, 150) + for i := 0; i < 150; i++ { + nodes[i] = v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-" + string(rune(i)), + Labels: map[string]string{}, + }, + } + } + + count := 0 + batchSizes := []int{} + batchFunc := func(batch []v1.Node, last bool) error { + count += len(batch) + batchSizes = append(batchSizes, len(batch)) + return nil + } + + err := batchOperate(nodes, batchFunc) + assert.NoError(t, err) + assert.Equal(t, 150, count) + // Should have two batches: 100 and 50 + assert.Equal(t, []int{100, 50}, batchSizes) + }) + + t.Run("error case", func(t *testing.T) { + nodes := make([]v1.Node, 150) + for i := 0; i < 150; i++ { + nodes[i] = v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-" + string(rune(i)), + Labels: map[string]string{}, + }, + } + } + + callCount := 0 + batchFunc := func(batch []v1.Node, last bool) error { + callCount++ + // Return error on second call to simulate failure in processing second batch + if callCount == 2 { + return errors.New("batch process failed") + } + return nil + } + + err := batchOperate(nodes, batchFunc) + assert.Error(t, err) + assert.Equal(t, "batch process failed", err.Error()) + }) + + t.Run("empty nodes", func(t *testing.T) { + nodes := []v1.Node{} + + count := 0 + batchFunc := func(batch []v1.Node, last bool) error { + count += len(batch) + return nil + } + + err := batchOperate(nodes, batchFunc) + assert.NoError(t, err) + assert.Equal(t, 0, count) + }) +} + +func TestNodeConditionReady(t *testing.T) { + t.Run("node with ready condition immediately", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-ready", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + Reason: "KubeletReady", + }, + }, + }, + } + client := fake.NewClientBuilder().WithRuntimeObjects(node).Build() + + condition := nodeConditionReady(client, node) + assert.NotNil(t, condition) + assert.Equal(t, v1.NodeReady, condition.Type) + assert.Equal(t, v1.ConditionTrue, condition.Status) + }) + + t.Run("node without ready condition initially but gets it after retry", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-retry", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{}, + }, + } + // Create a node that will have ready condition after first Get + nodeWithReady := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-retry", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + Reason: "KubeletReady", + }, + }, + }, + } + client := fake.NewClientBuilder().WithRuntimeObjects(nodeWithReady).Build() + + condition := nodeConditionReady(client, node) + // After retry, should find the condition + assert.NotNil(t, condition) + assert.Equal(t, v1.NodeReady, condition.Type) + }) + + t.Run("node deleted during retry", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-deleted", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{}, + }, + } + // Client without the node (simulating deletion) + client := fake.NewClientBuilder().Build() + + condition := nodeConditionReady(client, node) + assert.Nil(t, condition) + }) + + t.Run("node never gets ready condition", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-never-ready", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeMemoryPressure, + Status: v1.ConditionFalse, + }, + }, + }, + } + client := fake.NewClientBuilder().WithRuntimeObjects(node).Build() + + condition := nodeConditionReady(client, node) + // After all retries, should return nil + assert.Nil(t, condition) + }) + + t.Run("node with ready condition false", func(t *testing.T) { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node-not-ready", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionFalse, + Reason: "KubeletNotReady", + }, + }, + }, + } + client := fake.NewClientBuilder().WithRuntimeObjects(node).Build() + + condition := nodeConditionReady(client, node) + assert.NotNil(t, condition) + assert.Equal(t, v1.NodeReady, condition.Type) + assert.Equal(t, v1.ConditionFalse, condition.Status) + }) +} + +func getNodeCondition(node *v1.Node, conditionType v1.NodeConditionType) *v1.NodeCondition { + for _, condition := range node.Status.Conditions { + if condition.Type == conditionType { + return &condition + } + } + return nil +} diff --git a/pkg/controller/node/node_controller.go b/pkg/controller/node/node_controller.go index 2a1340c36..8d5295572 100644 --- a/pkg/controller/node/node_controller.go +++ b/pkg/controller/node/node_controller.go @@ -2,7 +2,6 @@ package node import ( "context" - "errors" "fmt" "time" @@ -88,7 +87,7 @@ func (controller nodeController) Start(ctx context.Context) error { for i := range ctrlCfg.CloudCFG.Global.NodeMaxConcurrentReconciles { go controller.recon.batchWorker(ctx, i) } - controller.recon.PeriodicalSync() + controller.recon.PeriodicalSync(ctx) return controller.c.Start(ctx) } @@ -120,7 +119,7 @@ func (m *ReconcileNode) Reconcile(ctx context.Context, request reconcile.Request err := m.client.Get(ctx, request.NamespacedName, node) if err != nil { if apierrors.IsNotFound(err) { - log.Info("node not found, skip", node, request.Name) + log.Info("node not found, skip", "node", request.Name) // Request object not found, cloud have been deleted // after reconcile request. // Owned objects are automatically garbage collected. @@ -189,81 +188,6 @@ outer: } } -func (m *ReconcileNode) syncCloudNode(node *corev1.Node) error { - cloudTaint := findCloudTaint(node.Spec.Taints) - if cloudTaint == nil { - klog.V(5).Infof("node %s is registered without cloud taint. return ok", node.Name) - return nil - } - - start := time.Now() - defer func() { - metric.NodeLatency.WithLabelValues("remove_taint").Observe(metric.MsSince(start)) - }() - - nodeRef := &corev1.ObjectReference{ - Kind: "Node", - Name: node.Name, - UID: types.UID(node.Name), - Namespace: "", - } - - err := m.doAddCloudNode(node) - if err != nil { - m.record.Event( - nodeRef, - corev1.EventTypeWarning, - helper.FailedAddNode, - fmt.Sprintf("Error adding node: %s", helper.GetLogMessage(err)), - ) - return fmt.Errorf("doAddCloudNode %s error: %s", node.Name, err.Error()) - } - m.record.Event(nodeRef, corev1.EventTypeNormal, helper.InitializedNode, "Initialize node successfully") - log.Info("Successfully initialized node", "node", node.Name) - return nil -} - -// This processes nodes that were added into the cluster, and cloud initialize them if appropriate -func (m *ReconcileNode) doAddCloudNode(node *corev1.Node) error { - instance, err := findCloudECS(m.cloud, node) - if err != nil { - if errors.Is(err, ErrNotFound) { - log.Info("cloud instance not found", "node", node.Name) - return nil - } - log.Error(err, "fail to find ecs", "node", node.Name) - return fmt.Errorf("find ecs: %s", err.Error()) - } - - // If user provided an IP address, ensure that IP address is found - // in the cloud provider before removing the taint on the node - if nodeIP, ok := isProvidedAddrExist(node, instance.Addresses); ok && nodeIP == nil { - return fmt.Errorf("failed to get specified nodeIP in cloud provider") - } - - initializer := func(_ context.Context) (done bool, err error) { - log.Info("try remove cloud taints", "node", node.Name) - - diff := func(copy runtime.Object) (client.Object, error) { - nins := copy.(*corev1.Node) - setFields(nins, instance, m.configCloudRoute, true) - return nins, nil - } - err = helper.PatchM(m.client, node, diff, helper.PatchAll) - if err != nil { - log.Error(err, "fail to patch node", "node", node.Name) - return false, nil - } - - log.Info("finished remove uninitialized cloud taints", "node", node.Name) - // After adding, call UpdateNodeAddress to set the CloudProvider provided IPAddresses - // So that users do not see any significant delay in IP addresses being filled into the node - _ = m.syncNode([]corev1.Node{*node}, false) - return true, nil - } - return wait.PollUntilContextTimeout(context.TODO(), 5*time.Second, 20*time.Second, true, initializer) -} - // syncNode sync the nodeAddress & cloud node existence func (m *ReconcileNode) syncNode(nodes []corev1.Node, configCloudRoute bool) error { start := time.Now() @@ -532,7 +456,7 @@ func (m *ReconcileNode) disableNetworkInterfaceSourceDestCheck(enis []eniInfo) ( return failed, nil } -func (m *ReconcileNode) PeriodicalSync() { +func (m *ReconcileNode) PeriodicalSync(ctx context.Context) { // Start a loop to periodically update the node addresses obtained from the cloud syncNode := func() { nodes, err := NodeList(m.client, false) @@ -552,5 +476,5 @@ func (m *ReconcileNode) PeriodicalSync() { log.Info("sync node successfully", "length", len(nodes.Items)) } - go wait.Until(syncNode, m.statusFrequency, wait.NeverStop) + go wait.Until(syncNode, m.statusFrequency, ctx.Done()) } diff --git a/pkg/controller/node/node_controller_test.go b/pkg/controller/node/node_controller_test.go index 56e78c5cc..a12c5b19c 100644 --- a/pkg/controller/node/node_controller_test.go +++ b/pkg/controller/node/node_controller_test.go @@ -8,75 +8,28 @@ import ( "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" + ctrlCfg "k8s.io/cloud-provider-alibaba-cloud/pkg/config" + "k8s.io/cloud-provider-alibaba-cloud/pkg/context/shared" prvd "k8s.io/cloud-provider-alibaba-cloud/pkg/provider" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" "k8s.io/cloud-provider-alibaba-cloud/pkg/util" "k8s.io/cloud-provider/api" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) var ( NodeName = fmt.Sprintf("cn-hangzhou.%s", vmock.InstanceIP) ) -func TestSyncCloudNode(t *testing.T) { - recon := getReconcileNode() - node := &v1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: NodeName, - }, - } - err := getFakeKubeClient().Get(context.TODO(), util.NamespacedName(node), node) - if err != nil { - t.Error(err) - } - - if err = recon.syncCloudNode(node); err != nil { - t.Error(err) - } - - updatedNode := &v1.Node{} - if err := recon.client.Get(context.TODO(), util.NamespacedName(node), updatedNode); err != nil { - t.Error(err) - } - - if len(updatedNode.Spec.Taints) != 0 { - t.Errorf("remove taint error, taints: %+v", updatedNode.Spec.Taints) - } - if instanceType, ok := updatedNode.Labels[v1.LabelInstanceType]; !ok || instanceType != vmock.InstanceType { - t.Errorf("node label LabelInstanceType not equal, expect %s, got %s", vmock.InstanceType, instanceType) - } - if zone, ok := updatedNode.Labels[v1.LabelTopologyZone]; !ok || zone != vmock.ZoneID { - t.Errorf("node label LabelTopologyZone not equal, expect %s, got %s", vmock.ZoneID, zone) - } - if region, ok := updatedNode.Labels[v1.LabelTopologyRegion]; !ok || region != vmock.RegionID { - t.Errorf("node label LabelTopologyRegion not equal, expect %s, got %s", vmock.RegionID, region) - } - if nodePoolID, ok := updatedNode.Labels[LabelNodePoolID]; !ok || nodePoolID != vmock.NodePoolID { - t.Errorf("node label LabelNodePoolID not equal, expect %s, got %s", vmock.NodePoolID, nodePoolID) - } - if instanceChargeType, ok := updatedNode.Labels[LabelInstanceChargeType]; !ok || instanceChargeType != vmock.InstanceChargeType { - t.Errorf("node label LabelInstanceChargeType not equal, expect %s, got %s", vmock.InstanceChargeType, instanceChargeType) - } - if spotStrategy, ok := updatedNode.Labels[LabelSpotStrategy]; !ok || spotStrategy != vmock.SpotStrategy { - t.Errorf("node label LabelSpotStrategy not equal, expect %s, got %s", vmock.SpotStrategy, spotStrategy) - } - if len(updatedNode.Status.Addresses) == 0 { - t.Error("node address is empty") - } - for _, addr := range updatedNode.Status.Addresses { - if addr.Type == v1.NodeInternalIP { - if addr.Address != vmock.InstanceIP { - t.Errorf("node internal ip address not equal, expect %s, got %s", vmock.InstanceIP, addr.Address) - } - } - } -} - func TestSyncLingJunNodes(t *testing.T) { cases := []struct { node v1.Node @@ -277,14 +230,864 @@ func TestSyncLingJunNodes(t *testing.T) { func getReconcileNode() *ReconcileNode { eventRecord := record.NewFakeRecorder(100) recon := &ReconcileNode{ - cloud: getMockCloudProvider(), - client: getFakeKubeClient(), - record: eventRecord, + cloud: getMockCloudProvider(), + client: getFakeKubeClient(), + record: eventRecord, + requestChan: make(chan *v1.Node, 10), } return recon } +func TestReconcileNode(t *testing.T) { + t.Run("node with cloud taint", func(t *testing.T) { + recon := getReconcileNode() + + req := reconcile.Request{ + NamespacedName: types.NamespacedName{Name: NodeName}, + } + + result, err := recon.Reconcile(context.TODO(), req) + assert.NoError(t, err) + assert.Equal(t, reconcile.Result{}, result) + + select { + case n := <-recon.requestChan: + assert.Equal(t, NodeName, n.Name) + default: + t.Error("Expected node to be added to request channel") + } + }) + + t.Run("node without cloud taint", func(t *testing.T) { + recon := getReconcileNode() + node := &v1.Node{} + err := recon.client.Get(context.TODO(), types.NamespacedName{Name: NodeName}, node) + assert.NoError(t, err) + + node.Spec.Taints = []v1.Taint{} + err = recon.client.Update(context.TODO(), node) + assert.NoError(t, err) + + req := reconcile.Request{ + NamespacedName: util.NamespacedName(node), + } + + // Test reconcile without cloud taint + result, err := recon.Reconcile(context.TODO(), req) + assert.NoError(t, err) + assert.Equal(t, reconcile.Result{}, result) + + // Verify nothing was added to request channel + select { + case n := <-recon.requestChan: + t.Errorf("Expected no node to be added to request channel, but got %s", n.Name) + default: + // This is expected + } + }) + + t.Run("nonexistent node", func(t *testing.T) { + recon := getReconcileNode() + + req := reconcile.Request{ + NamespacedName: client.ObjectKey{ + Name: "non-existent-node", + }, + } + + result, err := recon.Reconcile(context.TODO(), req) + assert.NoError(t, err) + assert.Equal(t, reconcile.Result{}, result) + + select { + case n := <-recon.requestChan: + t.Errorf("Expected no node to be added to request channel, but got %s", n.Name) + default: + // This is expected + } + }) +} + +func TestBatchWorker(t *testing.T) { + oldBatchSize := ctrlCfg.ControllerCFG.NodeReconcileBatchSize + oldAggregation := ctrlCfg.ControllerCFG.NodeEventAggregationWaitSeconds + defer func() { + time.Sleep(500 * time.Millisecond) + ctrlCfg.ControllerCFG.NodeReconcileBatchSize = oldBatchSize + ctrlCfg.ControllerCFG.NodeEventAggregationWaitSeconds = oldAggregation + }() + ctrlCfg.ControllerCFG.NodeReconcileBatchSize = 10 + ctrlCfg.ControllerCFG.NodeEventAggregationWaitSeconds = 0 + + t.Run("batch worker process nodes", func(t *testing.T) { + eventRecord := record.NewFakeRecorder(100) + recon := &ReconcileNode{ + cloud: getMockCloudProvider(), + client: getFakeKubeClient(), + record: eventRecord, + requestChan: make(chan *v1.Node, 10), + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go recon.batchWorker(ctx, 0) + + nodeList := &v1.NodeList{} + err := recon.client.List(context.TODO(), nodeList) + assert.NoError(t, err) + assert.NotEmpty(t, nodeList.Items) + + nodeCopy := nodeList.Items[0].DeepCopy() + recon.requestChan <- nodeCopy + + time.Sleep(2 * time.Second) + + updatedNode := &v1.Node{} + err = recon.client.Get(context.TODO(), types.NamespacedName{Name: nodeCopy.Name}, updatedNode) + assert.NoError(t, err) + + cloudTaint := findCloudTaint(updatedNode.Spec.Taints) + assert.Nil(t, cloudTaint) + }) + + t.Run("batch worker handle multiple nodes", func(t *testing.T) { + eventRecord := record.NewFakeRecorder(100) + recon := &ReconcileNode{ + cloud: getMockCloudProvider(), + client: getFakeKubeClient(), + record: eventRecord, + requestChan: make(chan *v1.Node, 10), + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go recon.batchWorker(ctx, 1) + + nodeList := &v1.NodeList{} + err := recon.client.List(context.TODO(), nodeList) + assert.NoError(t, err) + assert.NotEmpty(t, nodeList.Items) + + n1, n2 := nodeList.Items[0].DeepCopy(), nodeList.Items[0].DeepCopy() + recon.requestChan <- n1 + recon.requestChan <- n2 + + time.Sleep(2 * time.Second) + }) + + t.Run("batch worker context cancellation", func(t *testing.T) { + eventRecord := record.NewFakeRecorder(100) + recon := &ReconcileNode{ + cloud: getMockCloudProvider(), + client: getFakeKubeClient(), + record: eventRecord, + requestChan: make(chan *v1.Node, 10), + } + + ctx, cancel := context.WithCancel(context.Background()) + go recon.batchWorker(ctx, 2) + cancel() + time.Sleep(1 * time.Second) + }) + + t.Run("batch worker syncNode error", func(t *testing.T) { + eventRecord := record.NewFakeRecorder(100) + recon := &ReconcileNode{ + cloud: getMockCloudProvider(), + client: getFakeKubeClient(), + record: eventRecord, + requestChan: make(chan *v1.Node, 10), + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go recon.batchWorker(ctx, 3) + nodeWithListError := v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "node-list-instances-error"}, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-list-instances-error", + Taints: []v1.Taint{ + {Key: api.TaintExternalCloudProvider, Value: "true"}, + }, + }, + } + nodeCopy := nodeWithListError.DeepCopy() + recon.requestChan <- nodeCopy + time.Sleep(2 * time.Second) + }) + + t.Run("batch worker duplicated request", func(t *testing.T) { + eventRecord := record.NewFakeRecorder(100) + recon := &ReconcileNode{ + cloud: getMockCloudProvider(), + client: getFakeKubeClient(), + record: eventRecord, + requestChan: make(chan *v1.Node, 10), + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go recon.batchWorker(ctx, 4) + nodeList := &v1.NodeList{} + err := recon.client.List(context.TODO(), nodeList) + assert.NoError(t, err) + assert.NotEmpty(t, nodeList.Items) + base := nodeList.Items[0] + n1, n2, n3 := base.DeepCopy(), base.DeepCopy(), base.DeepCopy() + recon.requestChan <- n1 + recon.requestChan <- n2 + recon.requestChan <- n3 + time.Sleep(2 * time.Second) + }) + + time.Sleep(2 * time.Second) +} + +func TestSyncNode(t *testing.T) { + t.Run("normal case", func(t *testing.T) { + recon := getReconcileNode() + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: NodeName, + }, + } + err := getFakeKubeClient().Get(context.TODO(), util.NamespacedName(node), node) + if err != nil { + t.Error(err) + } + + if err = recon.syncNode([]v1.Node{*node}, true); err != nil { + t.Error(err) + } + + updatedNode := &v1.Node{} + if err := recon.client.Get(context.TODO(), util.NamespacedName(node), updatedNode); err != nil { + t.Error(err) + } + + if len(updatedNode.Spec.Taints) != 0 { + t.Errorf("remove taint error, taints: %+v", updatedNode.Spec.Taints) + } + if instanceType, ok := updatedNode.Labels[v1.LabelInstanceType]; !ok || instanceType != vmock.InstanceType { + t.Errorf("node label LabelInstanceType not equal, expect %s, got %s", vmock.InstanceType, instanceType) + } + if zone, ok := updatedNode.Labels[v1.LabelTopologyZone]; !ok || zone != vmock.ZoneID { + t.Errorf("node label LabelTopologyZone not equal, expect %s, got %s", vmock.ZoneID, zone) + } + if region, ok := updatedNode.Labels[v1.LabelTopologyRegion]; !ok || region != vmock.RegionID { + t.Errorf("node label LabelTopologyRegion not equal, expect %s, got %s", vmock.RegionID, region) + } + if nodePoolID, ok := updatedNode.Labels[LabelNodePoolID]; !ok || nodePoolID != vmock.NodePoolID { + t.Errorf("node label LabelNodePoolID not equal, expect %s, got %s", vmock.NodePoolID, nodePoolID) + } + if instanceChargeType, ok := updatedNode.Labels[LabelInstanceChargeType]; !ok || instanceChargeType != vmock.InstanceChargeType { + t.Errorf("node label LabelInstanceChargeType not equal, expect %s, got %s", vmock.InstanceChargeType, instanceChargeType) + } + if spotStrategy, ok := updatedNode.Labels[LabelSpotStrategy]; !ok || spotStrategy != vmock.SpotStrategy { + t.Errorf("node label LabelSpotStrategy not equal, expect %s, got %s", vmock.SpotStrategy, spotStrategy) + } + if len(updatedNode.Status.Addresses) == 0 { + t.Error("node address is empty") + } + for _, addr := range updatedNode.Status.Addresses { + if addr.Type == v1.NodeInternalIP { + if addr.Address != vmock.InstanceIP { + t.Errorf("node internal ip address not equal, expect %s, got %s", vmock.InstanceIP, addr.Address) + } + } + } + }) + + t.Run("unrecognized node type skipped", func(t *testing.T) { + recon := getReconcileNode() + unknownNode := v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "unknown-node"}, + Spec: v1.NodeSpec{ProviderID: ""}, + } + err := recon.syncNode([]v1.Node{unknownNode}, true) + assert.NoError(t, err) + }) + + t.Run("node without cloud taint", func(t *testing.T) { + recon := getReconcileNode() + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-without-cloud-taint", + }, + Spec: v1.NodeSpec{ + PodCIDR: "10.96.0.64/26", + ProviderID: "cn-hangzhou.ecs-id-1", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + + err := recon.client.Create(context.TODO(), node) + assert.NoError(t, err) + + err = recon.syncNode([]v1.Node{*node}, true) + assert.NoError(t, err) + + n := &v1.Node{} + err = recon.client.Get(context.TODO(), util.NamespacedName(node), n) + assert.NoError(t, err) + }) + + t.Run("not exists on cloud with status unknown", func(t *testing.T) { + recon := getReconcileNode() + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-not-exists", + }, + Spec: v1.NodeSpec{ + PodCIDR: "10.96.0.64/26", + ProviderID: "cn-hangzhou.not-exists-ecs-id", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionUnknown, + }, + }, + }, + } + + err := recon.client.Create(context.TODO(), node) + assert.NoError(t, err) + + err = recon.syncNode([]v1.Node{*node}, true) + assert.NoError(t, err) + + // wait for node to be deleted + time.Sleep(100 * time.Millisecond) + + n := &v1.Node{} + err = recon.client.Get(context.TODO(), util.NamespacedName(node), n) + assert.Error(t, err) + assert.True(t, errors.IsNotFound(err)) + }) + + t.Run("not exists on cloud with status ready", func(t *testing.T) { + recon := getReconcileNode() + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-not-exists-1", + }, + Spec: v1.NodeSpec{ + PodCIDR: "10.96.0.64/26", + ProviderID: "cn-hangzhou.not-exists-ecs-id-1", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + + err := recon.client.Create(context.TODO(), node) + assert.NoError(t, err) + + err = recon.syncNode([]v1.Node{*node}, true) + assert.NoError(t, err) + + n := &v1.Node{} + err = recon.client.Get(context.TODO(), util.NamespacedName(node), n) + assert.NoError(t, err) + }) + + t.Run("node with source dest check disabled", func(t *testing.T) { + // Save original config value + originalValue := ctrlCfg.ControllerCFG.SkipDisableSourceDestCheck + ctrlCfg.ControllerCFG.SkipDisableSourceDestCheck = true + defer func() { + ctrlCfg.ControllerCFG.SkipDisableSourceDestCheck = originalValue + }() + + recon := getReconcileNode() + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: NodeName, + }, + } + err := getFakeKubeClient().Get(context.TODO(), util.NamespacedName(node), node) + assert.NoError(t, err) + + // Ensure node has cloud taint to trigger source dest check logic + node.Spec.Taints = append(node.Spec.Taints, v1.Taint{ + Key: api.TaintExternalCloudProvider, + Value: "true", + }) + + err = recon.client.Update(context.TODO(), node) + assert.NoError(t, err) + + err = recon.syncNode([]v1.Node{*node}, true) + assert.NoError(t, err) + + // Verify node was processed correctly + updatedNode := &v1.Node{} + err = recon.client.Get(context.TODO(), util.NamespacedName(node), updatedNode) + assert.NoError(t, err) + }) + + t.Run("multiple nodes processing", func(t *testing.T) { + recon := getReconcileNode() + + // Get existing node + existingNode := &v1.Node{} + err := getFakeKubeClient().Get(context.TODO(), types.NamespacedName{Name: NodeName}, existingNode) + assert.NoError(t, err) + + // Create additional node + additionalNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "additional-node", + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-additional", + Taints: []v1.Taint{ + {Key: api.TaintExternalCloudProvider, Value: "true", Effect: v1.TaintEffectNoSchedule}, + }, + }, + } + err = recon.client.Create(context.TODO(), additionalNode) + assert.NoError(t, err) + + nodes := []v1.Node{*existingNode, *additionalNode} + err = recon.syncNode(nodes, true) + assert.NoError(t, err) + + // Verify both nodes were processed + for _, node := range nodes { + updatedNode := &v1.Node{} + err = recon.client.Get(context.TODO(), util.NamespacedName(&node), updatedNode) + assert.NoError(t, err) + + // Cloud taint should be removed + cloudTaint := findCloudTaint(updatedNode.Spec.Taints) + assert.Nil(t, cloudTaint) + } + }) + + t.Run("node with user specified ip", func(t *testing.T) { + recon := getReconcileNode() + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-with-user-ip", + Annotations: map[string]string{ + "alpha.kubernetes.io/provided-node-ip": vmock.InstanceIP, + }, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-user-ip", + Taints: []v1.Taint{ + {Key: api.TaintExternalCloudProvider, Value: "true", Effect: v1.TaintEffectNoSchedule}, + }, + }, + } + err := recon.client.Create(context.TODO(), node) + assert.NoError(t, err) + + err = recon.syncNode([]v1.Node{*node}, true) + assert.NoError(t, err) + + updatedNode := &v1.Node{} + err = recon.client.Get(context.TODO(), util.NamespacedName(node), updatedNode) + assert.NoError(t, err) + + // Should have only the user-specified IP address + assert.Len(t, updatedNode.Status.Addresses, 1) + assert.Equal(t, v1.NodeInternalIP, updatedNode.Status.Addresses[0].Type) + assert.Equal(t, vmock.InstanceIP, updatedNode.Status.Addresses[0].Address) + }) + + t.Run("node with invalid user specified ip", func(t *testing.T) { + recon := getReconcileNode() + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-with-invalid-user-ip", + Annotations: map[string]string{ + "alpha.kubernetes.io/provided-node-ip": "1.2.3.4", // IP that doesn't match cloud provider + }, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-invalid-user-ip", + Taints: []v1.Taint{ + {Key: api.TaintExternalCloudProvider, Value: "true", Effect: v1.TaintEffectNoSchedule}, + }, + }, + } + err := recon.client.Create(context.TODO(), node) + assert.NoError(t, err) + + err = recon.syncNode([]v1.Node{*node}, true) + assert.NoError(t, err) + + updatedNode := &v1.Node{} + err = recon.client.Get(context.TODO(), util.NamespacedName(node), updatedNode) + assert.NoError(t, err) + }) + + t.Run("patch node status error", func(t *testing.T) { + recon := getReconcileNode() + + // Create a node + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "patch-error-node", + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-patch-error", + Taints: []v1.Taint{ + {Key: api.TaintExternalCloudProvider, Value: "true", Effect: v1.TaintEffectNoSchedule}, + }, + }, + } + err := recon.client.Create(context.TODO(), node) + assert.NoError(t, err) + + // Use a client that will fail on patch operations + mockClient := &MockClientWithPatchError{ + Client: recon.client, + } + recon.client = mockClient + + err = recon.syncNode([]v1.Node{*node}, true) + assert.NoError(t, err) // syncNode doesn't return error even if patch fails + + // But the node should still exist + updatedNode := &v1.Node{} + err = mockClient.Get(context.TODO(), util.NamespacedName(node), updatedNode) + assert.NoError(t, err) + }) +} + +func TestDisableNetworkInterfaceSourceDestCheck(t *testing.T) { + t.Run("with API error", func(t *testing.T) { + enis := []eniInfo{ + { + ENI: "eni-error", + NodeRef: &v1.ObjectReference{ + Kind: "Node", + Name: "test", + }, + }, + } + + recon := getReconcileNode() + failed, err := recon.disableNetworkInterfaceSourceDestCheck(enis) + assert.NoError(t, err) + assert.Len(t, failed, 1) + }) +} + +// MockClientWithPatchError implements client.Client but returns error for Patch operations +type MockClientWithPatchError struct { + client.Client +} + +func (m *MockClientWithPatchError) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + // Return error for patch operations + return fmt.Errorf("simulated patch error") +} + +func (m *MockClientWithPatchError) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + return m.Client.Get(ctx, key, obj, opts...) +} + +func TestIsProvidedAddrExist(t *testing.T) { + nodeWithProvidedAddr := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "alpha.kubernetes.io/provided-node-ip": "192.168.1.100", + }, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + {Type: v1.NodeExternalIP, Address: "1.2.3.4"}, + }, + }, + } + + nodeWithoutProvidedAddr := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + }, + }, + } + + addressesWithMatch := []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + {Type: v1.NodeExternalIP, Address: "1.2.3.4"}, + } + + addressesWithoutMatch := []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.101"}, + {Type: v1.NodeExternalIP, Address: "1.2.3.5"}, + } + + tests := []struct { + name string + node *v1.Node + addresses []v1.NodeAddress + expectedOK bool + expectedAddr *v1.NodeAddress + }{ + { + name: "provided addr exists and matches", + node: nodeWithProvidedAddr, + addresses: addressesWithMatch, + expectedOK: true, + expectedAddr: &v1.NodeAddress{Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + }, + { + name: "provided addr exists but no match", + node: nodeWithProvidedAddr, + addresses: addressesWithoutMatch, + expectedOK: true, + expectedAddr: nil, + }, + { + name: "no provided addr", + node: nodeWithoutProvidedAddr, + addresses: addressesWithMatch, + expectedOK: false, + expectedAddr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr, ok := isProvidedAddrExist(tt.node, tt.addresses) + assert.Equal(t, tt.expectedOK, ok) + if tt.expectedAddr == nil { + assert.Nil(t, addr) + } else { + assert.NotNil(t, addr) + assert.Equal(t, tt.expectedAddr.Type, addr.Type) + assert.Equal(t, tt.expectedAddr.Address, addr.Address) + } + }) + } +} + +func TestSetHostnameAddress(t *testing.T) { + nodeWithoutHostname := &v1.Node{ + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + }, + }, + } + + nodeWithHostname := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "alpha.kubernetes.io/provided-node-ip": "192.168.1.100", + }, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + {Type: v1.NodeHostName, Address: "test-hostname"}, + }, + }, + } + + addressWithHostname := []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + {Type: v1.NodeHostName, Address: "cloud-hostname"}, + } + + addressWithoutHostname := []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + {Type: v1.NodeExternalIP, Address: "1.2.3.4"}, + } + + tests := []struct { + name string + node *v1.Node + addresses []v1.NodeAddress + expectedCount int + expectedHostname string + hasHostname bool + }{ + { + name: "add hostname when not present", + node: nodeWithHostname, + addresses: addressWithoutHostname, + expectedCount: 3, + expectedHostname: "test-hostname", + hasHostname: true, + }, + { + name: "keep existing hostname", + node: nodeWithHostname, + addresses: addressWithHostname, + expectedCount: 2, + expectedHostname: "cloud-hostname", + hasHostname: true, + }, + { + name: "no hostname", + node: nodeWithoutHostname, + addresses: addressWithoutHostname, + expectedCount: 2, + hasHostname: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := setHostnameAddress(tt.node, tt.addresses) + assert.Equal(t, tt.expectedCount, len(result)) + + hasHostname := false + for _, addr := range result { + if addr.Type == v1.NodeHostName { + hasHostname = true + if tt.expectedHostname != "" { + assert.Equal(t, tt.expectedHostname, addr.Address) + } + break + } + } + assert.Equal(t, tt.hasHostname, hasHostname) + }) + } +} + +func TestSetFields(t *testing.T) { + cloudNode := &prvd.NodeAttribute{ + InstanceID: "i-test", + InstanceType: "ecs.c1m1.large", + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.100"}, + }, + Zone: "cn-hangzhou-a", + Region: "cn-hangzhou", + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + Spec: v1.NodeSpec{ + Taints: []v1.Taint{ + {Key: api.TaintExternalCloudProvider, Value: "true", Effect: v1.TaintEffectNoSchedule}, + }, + }, + } + + // Test with removeCloudTaint=true + setFields(node, cloudNode, true, true) + + // Check labels + assert.Equal(t, "ecs.c1m1.large", node.Labels[v1.LabelInstanceTypeStable]) + assert.Equal(t, "cn-hangzhou-a", node.Labels[v1.LabelTopologyZone]) + assert.Equal(t, "cn-hangzhou", node.Labels[v1.LabelTopologyRegion]) + + // Check that cloud taint was removed + hasCloudTaint := false + for _, taint := range node.Spec.Taints { + if taint.Key == api.TaintExternalCloudProvider { + hasCloudTaint = true + break + } + } + assert.False(t, hasCloudTaint) + + // Reset node for second test + node.Spec.Taints = []v1.Taint{ + {Key: api.TaintExternalCloudProvider, Value: "true", Effect: v1.TaintEffectNoSchedule}, + } + + // Test with removeCloudTaint=false + setFields(node, cloudNode, true, false) + + // Check that cloud taint was not removed + hasCloudTaint = false + for _, taint := range node.Spec.Taints { + if taint.Key == api.TaintExternalCloudProvider { + hasCloudTaint = true + break + } + } + assert.True(t, hasCloudTaint) +} + +func TestAdd(t *testing.T) { + mgr, err := manager.New(&rest.Config{}, manager.Options{}) + assert.NoError(t, err) + + assert.NotPanics(t, func() { + err := Add(mgr, &shared.SharedContext{}) + assert.NoError(t, err) + }) +} + +func TestPeriodicalSync(t *testing.T) { + t.Run("normal case", func(t *testing.T) { + recon := getReconcileNode() + recon.statusFrequency = 100 * time.Millisecond + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + recon.PeriodicalSync(ctx) + time.Sleep(200 * time.Millisecond) + }) + + t.Run("NodeList error", func(t *testing.T) { + recon := getReconcileNode() + recon.statusFrequency = 50 * time.Millisecond + recon.client = &MockClientWithListError{ + Client: recon.client, + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + recon.PeriodicalSync(ctx) + time.Sleep(100 * time.Millisecond) + }) + + t.Run("context cancellation", func(t *testing.T) { + recon := getReconcileNode() + recon.statusFrequency = 50 * time.Millisecond + ctx, cancel := context.WithCancel(context.Background()) + + recon.PeriodicalSync(ctx) + time.Sleep(50 * time.Millisecond) + cancel() + time.Sleep(100 * time.Millisecond) + }) +} + +type MockClientWithListError struct { + client.Client +} + +func (m *MockClientWithListError) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + return fmt.Errorf("simulated list error") +} + func getMockCloudProvider() prvd.Provider { return vmock.MockCloud{ MockVPC: vmock.NewMockVPC(nil), diff --git a/pkg/controller/route/event_handler_test.go b/pkg/controller/route/event_handler_test.go new file mode 100644 index 000000000..ad6d8ecd5 --- /dev/null +++ b/pkg/controller/route/event_handler_test.go @@ -0,0 +1,278 @@ +package route + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestEnqueueRequestForNodeEvent_Create(t *testing.T) { + // Create a rate limiter + rateLimiter := workqueue.DefaultControllerRateLimiter() + + // Create the event handler + handler := &enqueueRequestForNodeEvent{ + rateLimiter: rateLimiter, + } + + // Create a test node + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + } + + // Create a fake queue + queue := workqueue.NewRateLimitingQueue(rateLimiter) + + // Create an event + createEvent := event.CreateEvent{ + Object: node, + } + + // Call the Create method + handler.Create(context.TODO(), createEvent, queue) + + // Verify the queue has one item + assert.Equal(t, 1, queue.Len()) + + // Get the item from the queue + item, shutdown := queue.Get() + assert.False(t, shutdown) + + // Verify the item is a reconcile.Request with correct name + request, ok := item.(reconcile.Request) + assert.True(t, ok) + assert.Equal(t, "test-node", request.Name) + + // Done with the item + queue.Done(item) +} + +func TestEnqueueRequestForNodeEvent_Update(t *testing.T) { + // Create a rate limiter + rateLimiter := workqueue.DefaultControllerRateLimiter() + + // Create the event handler + handler := &enqueueRequestForNodeEvent{ + rateLimiter: rateLimiter, + } + + // Create test nodes + oldNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "old-node", + }, + } + + newNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-node", + }, + } + + // Create a fake queue + queue := workqueue.NewRateLimitingQueue(rateLimiter) + + // Create an update event + updateEvent := event.UpdateEvent{ + ObjectOld: oldNode, + ObjectNew: newNode, + } + + // Call the Update method + handler.Update(context.TODO(), updateEvent, queue) + + // Verify the queue has one item + assert.Equal(t, 1, queue.Len()) + + // Get the item from the queue + item, shutdown := queue.Get() + assert.False(t, shutdown) + + // Verify the item is a reconcile.Request with correct name (should be new node name) + request, ok := item.(reconcile.Request) + assert.True(t, ok) + assert.Equal(t, "new-node", request.Name) + + // Done with the item + queue.Done(item) +} + +func TestEnqueueRequestForNodeEvent_Delete(t *testing.T) { + // Create a rate limiter + rateLimiter := workqueue.DefaultControllerRateLimiter() + + // Create the event handler + handler := &enqueueRequestForNodeEvent{ + rateLimiter: rateLimiter, + } + + // Create a test node + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deleted-node", + }, + } + + // Create a fake queue + queue := workqueue.NewRateLimitingQueue(rateLimiter) + + // Create a delete event + deleteEvent := event.DeleteEvent{ + Object: node, + } + + // Call the Delete method + handler.Delete(context.TODO(), deleteEvent, queue) + + // Verify the queue has one item + assert.Equal(t, 1, queue.Len()) + + // Get the item from the queue + item, shutdown := queue.Get() + assert.False(t, shutdown) + + // Verify the item is a reconcile.Request with correct name + request, ok := item.(reconcile.Request) + assert.True(t, ok) + assert.Equal(t, "deleted-node", request.Name) + + // Done with the item + queue.Done(item) +} + +func TestEnqueueRequestForNodeEvent_Generic(t *testing.T) { + // Create a rate limiter + rateLimiter := workqueue.DefaultControllerRateLimiter() + + // Create the event handler + handler := &enqueueRequestForNodeEvent{ + rateLimiter: rateLimiter, + } + + // Create a test node + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "generic-node", + }, + } + + // Create a fake queue + queue := workqueue.NewRateLimitingQueue(rateLimiter) + + // Create a generic event + genericEvent := event.GenericEvent{ + Object: node, + } + + // Call the Generic method + handler.Generic(context.TODO(), genericEvent, queue) + + // Need to wait a bit for the AddAfter to process + time.Sleep(100 * time.Millisecond) + + // Verify the queue has one item + assert.Equal(t, 1, queue.Len()) + + // Get the item from the queue + item, shutdown := queue.Get() + assert.False(t, shutdown) + + // Verify the item is a reconcile.Request with correct name + request, ok := item.(reconcile.Request) + assert.True(t, ok) + assert.Equal(t, "generic-node", request.Name) + + // Done with the item + queue.Done(item) +} + +func TestEnqueueRequestForNodeEvent_NonNodeObjects(t *testing.T) { + // Create a rate limiter + rateLimiter := workqueue.DefaultControllerRateLimiter() + + // Create the event handler + handler := &enqueueRequestForNodeEvent{ + rateLimiter: rateLimiter, + } + + // Create a fake queue + queue := workqueue.NewRateLimitingQueue(rateLimiter) + + // Test with non-Node object in Create event + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + } + + createEvent := event.CreateEvent{ + Object: pod, + } + + // Call the Create method with non-Node object + handler.Create(context.TODO(), createEvent, queue) + + // Verify the queue is empty + assert.Equal(t, 0, queue.Len()) + + // Test with non-Node object in Update event + oldPod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "old-pod", + Namespace: "default", + }, + } + + newPod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-pod", + Namespace: "default", + }, + } + + updateEvent := event.UpdateEvent{ + ObjectOld: oldPod, + ObjectNew: newPod, + } + + // Call the Update method with non-Node object + handler.Update(context.TODO(), updateEvent, queue) + + // Verify the queue is still empty + assert.Equal(t, 0, queue.Len()) + + // Test with non-Node object in Delete event + deleteEvent := event.DeleteEvent{ + Object: pod, + } + + // Call the Delete method with non-Node object + handler.Delete(context.TODO(), deleteEvent, queue) + + // Verify the queue is still empty + assert.Equal(t, 0, queue.Len()) + + // Test with non-Node object in Generic event + genericEvent := event.GenericEvent{ + Object: pod, + } + + // Call the Generic method with non-Node object + handler.Generic(context.TODO(), genericEvent, queue) + + // Need to wait a bit for the AddAfter to process + time.Sleep(100 * time.Millisecond) + + // Verify the queue is still empty + assert.Equal(t, 0, queue.Len()) +} diff --git a/pkg/controller/route/manager_test.go b/pkg/controller/route/manager_test.go index 227b83962..fc620c7d5 100644 --- a/pkg/controller/route/manager_test.go +++ b/pkg/controller/route/manager_test.go @@ -3,15 +3,27 @@ package route import ( "context" "fmt" + "net" + "testing" + "time" + + cmap "github.com/orcaman/concurrent-map" "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" globalCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/config" + "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" "k8s.io/cloud-provider-alibaba-cloud/pkg/model" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" - "net" - "testing" + "sigs.k8s.io/controller-runtime/pkg/event" ) func TestGetRouteTables(t *testing.T) { + // Setup test cases with different VPC configurations + globalCtx.CloudCFG.Global.VpcID = "vpc-test-id" + noRouteTableVPC := vmock.MockCloud{ MockVPC: vmock.NewMockVPC(nil), IMetaData: vmock.NewMockMetaData("vpc-no-route-table"), @@ -25,25 +37,37 @@ func TestGetRouteTables(t *testing.T) { IMetaData: vmock.NewMockMetaData("vpc-multi-route-table"), } + // Test with route table IDs specified in config globalCtx.CloudCFG.Global.RouteTableIDS = "table-xxx,table-yyy" tables, err := getRouteTables(context.Background(), noRouteTableVPC) assert.Equal(t, 2, len(tables), "assert route tables from cloud config") - assert.Equal(t, nil, err, "assert route tables from cloud config") + assert.Equal(t, []string{"table-xxx", "table-yyy"}, tables, "assert route tables from cloud config") + assert.NoError(t, err, "assert route tables from cloud config") + // Test with no route tables in VPC and no config globalCtx.CloudCFG.Global.RouteTableIDS = "" tables, err = getRouteTables(context.Background(), noRouteTableVPC) assert.Equal(t, 0, len(tables), "assert route tables from no table vpc") - assert.Equal(t, false, err == nil, "assert route tables from no table vpc") + assert.Error(t, err, "assert route tables from no table vpc") + assert.Equal(t, "no route tables found by vpc id[vpc-test-id]", err.Error(), "assert route tables from no table vpc") + // Test with single route table in VPC globalCtx.CloudCFG.Global.RouteTableIDS = "" tables, err = getRouteTables(context.Background(), singleRouteTableVPC) assert.Equal(t, 1, len(tables), "assert route tables from no single vpc") - assert.Equal(t, nil, err, "assert route tables from single table vpc") + assert.Equal(t, []string{"route-table-1"}, tables, "assert route tables from single table vpc") + assert.NoError(t, err, "assert route tables from single table vpc") + // Test with multiple route tables in VPC globalCtx.CloudCFG.Global.RouteTableIDS = "" tables, err = getRouteTables(context.Background(), multiRouteTableVPC) assert.Equal(t, 0, len(tables), "assert route tables from multi table vpc") - assert.Equal(t, false, err == nil, "assert route tables from multi table vpc") + assert.Error(t, err, "assert route tables from multi table vpc") + assert.Equal(t, "multiple route tables found by vpc id[vpc-test-id], length(tables)=2", err.Error(), "assert route tables from multi table vpc") + + // Reset global config + globalCtx.CloudCFG.Global.RouteTableIDS = "" + globalCtx.CloudCFG.Global.VpcID = "" } func TestContainsRoute(t *testing.T) { @@ -139,3 +163,493 @@ func TestFindRouteCached(t *testing.T) { assert.Equal(t, testcase.err, err != nil) } } + +func TestConflictWithNodes(t *testing.T) { + // Create test node list + nodes := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Spec: v1.NodeSpec{ + ProviderID: "i-123", + PodCIDR: "192.168.1.0/24", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + Spec: v1.NodeSpec{ + ProviderID: "i-456", + PodCIDR: "192.168.2.0/24", + PodCIDRs: []string{"192.168.3.0/24"}, + }, + }, + }, + } + + testCases := []struct { + name string + route *model.Route + expected bool + }{ + { + name: "Conflict route - Same CIDR but different ProviderID", + route: &model.Route{ + DestinationCIDR: "192.168.1.0/24", + ProviderId: "i-789", // Same CIDR as node1 but different ProviderID + }, + expected: true, + }, + { + name: "Conflict route - Conflict with PodCIDRs", + route: &model.Route{ + DestinationCIDR: "192.168.3.0/24", + ProviderId: "i-789", // Same CIDR as node1 but different ProviderID + }, + expected: true, + }, + { + name: "Non-conflict route - Both CIDR and ProviderID match", + route: &model.Route{ + DestinationCIDR: "192.168.1.0/24", + ProviderId: "i-123", // Complete match with node1 + }, + expected: false, + }, + { + name: "Non-conflict route - CIDR does not match", + route: &model.Route{ + DestinationCIDR: "10.0.0.0/24", + ProviderId: "i-789", + }, + expected: false, + }, + { + name: "Conflict route - Subnet inclusion relationship", + route: &model.Route{ + DestinationCIDR: "192.168.1.0/30", // Contains node1's CIDR + ProviderId: "i-789", + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := conflictWithNodes(tc.route, nodes) + assert.Equal(t, tc.expected, result) + }) + } + + t.Run("node with invalid PodCIDR", func(t *testing.T) { + invalidNodes := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{Name: "bad-node"}, + Spec: v1.NodeSpec{ + ProviderID: "i-999", + PodCIDR: "invalid-cidr", + }, + }, + }, + } + route := &model.Route{DestinationCIDR: "10.0.0.0/24", ProviderId: "i-999"} + result := conflictWithNodes(route, invalidNodes) + assert.False(t, result) + }) + + t.Run("node with no PodCIDR", func(t *testing.T) { + noCIDRNodes := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{Name: "no-cidr-node"}, + Spec: v1.NodeSpec{ProviderID: "i-888"}, + }, + }, + } + route := &model.Route{DestinationCIDR: "10.0.0.0/24", ProviderId: "i-888"} + result := conflictWithNodes(route, noCIDRNodes) + assert.False(t, result) + }) + + t.Run("route with unparsable DestinationCIDR", func(t *testing.T) { + nodes := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node1"}, + Spec: v1.NodeSpec{ProviderID: "i-123", PodCIDR: "192.168.1.0/24"}, + }, + }, + } + route := &model.Route{DestinationCIDR: "invalid-cidr", ProviderId: "i-123"} + result := conflictWithNodes(route, nodes) + assert.False(t, result) + }) +} + +func TestCreateRouteForInstance(t *testing.T) { + ctx := context.Background() + mockVPC := vmock.NewMockVPC(nil) + route, err := createRouteForInstance(ctx, "route-table-1", "i-123", "10.96.0.0/24", mockVPC) + assert.NoError(t, err) + assert.NotNil(t, route) + assert.Equal(t, "10.96.0.0/24", route.DestinationCIDR) + assert.Equal(t, "i-123", route.ProviderId) + + t.Run("duplicate CIDR FindRoute succeeds", func(t *testing.T) { + route2, err2 := createRouteForInstance(ctx, "route-table-1", "i-duplicate", "10.96.0.0/24", mockVPC) + assert.NoError(t, err2) + assert.NotNil(t, route2) + assert.Equal(t, "i-duplicate", route2.ProviderId) + }) + + t.Run("duplicate CIDR FindRoute returns nil", func(t *testing.T) { + _, err := createRouteForInstance(ctx, "route-table-1", "i-dup-find-fail", "10.96.0.0/24", mockVPC) + assert.Error(t, err) + assert.Contains(t, err.Error(), "InvalidCIDRBlock.Duplicate") + }) +} + +func TestDeleteRouteForInstance(t *testing.T) { + ctx := context.Background() + mockVPC := vmock.NewMockVPC(nil) + err := deleteRouteForInstance(ctx, "route-table-1", "i-123", "10.96.0.0/24", mockVPC) + assert.NoError(t, err) +} + +func TestFindRoute(t *testing.T) { + ctx := context.Background() + mockVPC := vmock.NewMockVPC(nil) + + _, err := findRoute(ctx, "t", "", "", nil, mockVPC) + assert.Error(t, err) + assert.Contains(t, err.Error(), "empty query condition") + + cached := []*model.Route{ + {DestinationCIDR: "10.96.0.0/24", ProviderId: "i-1", Name: "r1"}, + {DestinationCIDR: "10.96.0.128/26", ProviderId: "i-2", Name: "r2"}, + } + r, err := findRoute(ctx, "t", "i-1", "10.96.0.0/24", cached, mockVPC) + assert.NoError(t, err) + assert.NotNil(t, r) + assert.Equal(t, "i-1", r.ProviderId) + + r, err = findRoute(ctx, "t", "i-2", "", cached, mockVPC) + assert.NoError(t, err) + assert.NotNil(t, r) + assert.Equal(t, "i-2", r.ProviderId) + + r, err = findRoute(ctx, "t", "", "10.96.0.128/26", cached, mockVPC) + assert.NoError(t, err) + assert.NotNil(t, r) + assert.Equal(t, "10.96.0.128/26", r.DestinationCIDR) + + r, err = findRoute(ctx, "t", "i-99", "10.0.0.0/24", cached, mockVPC) + assert.NoError(t, err) + assert.Nil(t, r) + + r, err = findRoute(ctx, "t", "i-123", "192.168.1.0/24", nil, mockVPC) + assert.NoError(t, err) + assert.NotNil(t, r) +} + +func TestNeedSyncRoute(t *testing.T) { + // Normal node + normalNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "normal-node", + }, + Spec: v1.NodeSpec{ + ProviderID: "i-123", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + + // Node with exclusion label + excludedNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "excluded-node", + Labels: map[string]string{ + helper.LabelNodeExcludeNode: "true", + }, + }, + Spec: v1.NodeSpec{ + ProviderID: "i-456", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + + // Node with unknown status + unknownNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unknown-node", + }, + Spec: v1.NodeSpec{ + ProviderID: "i-789", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionUnknown, + }, + }, + }, + } + + // Node being deleted + deletingNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deleting-node", + DeletionTimestamp: &metav1.Time{}, + }, + Spec: v1.NodeSpec{ + ProviderID: "i-101", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + + // Node without ProviderID + noProviderNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-provider-node", + }, + Spec: v1.NodeSpec{ + ProviderID: "", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + + testCases := []struct { + name string + node *v1.Node + expected bool + }{ + { + name: "Normal node", + node: normalNode, + expected: true, + }, + { + name: "Node with exclusion label", + node: excludedNode, + expected: false, + }, + { + name: "Node with unknown status", + node: unknownNode, + expected: false, + }, + { + name: "Node being deleted", + node: deletingNode, + expected: false, + }, + { + name: "Node without ProviderID", + node: noProviderNode, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := needSyncRoute(tc.node) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestFindRouteWithoutCache(t *testing.T) { + // Create mock provider with predefined routes + mockProvider := &vmock.MockVPC{} + + testCases := []struct { + name string + table string + pvid string + cidr string + expectFound bool + expectName string + expectPVID string + expectCIDR string + expectError bool + }{ + { + name: "Find by ProviderID and CIDR", + table: "table-1", + pvid: "i-123", + cidr: "192.168.1.0/24", + expectFound: true, + expectName: "route-1", + expectPVID: "i-123", + expectCIDR: "192.168.1.0/24", + expectError: false, + }, + { + name: "Find by ProviderID only", + table: "table-1", + pvid: "i-456", + cidr: "", + expectFound: true, + expectName: "route-2", + expectPVID: "i-456", + expectCIDR: "192.168.2.0/24", + expectError: false, + }, + { + name: "Find by CIDR only", + table: "table-1", + pvid: "", + cidr: "192.168.1.0/24", + expectFound: true, + expectName: "route-1", + expectPVID: "i-123", + expectCIDR: "192.168.1.0/24", + expectError: false, + }, + { + name: "No matching route found", + table: "table-1", + pvid: "i-999", + cidr: "10.0.0.0/24", + expectFound: false, + expectError: false, + }, + { + name: "Empty query conditions", + table: "table-1", + pvid: "", + cidr: "", + expectFound: false, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + route, err := findRoute(context.Background(), tc.table, tc.pvid, tc.cidr, nil, mockProvider) + + if tc.expectError { + assert.Error(t, err) + assert.Nil(t, route) + } else { + assert.NoError(t, err) + } + + if tc.expectFound { + assert.NotNil(t, route) + if tc.expectName != "" { + assert.Equal(t, tc.expectName, route.Name) + } + if tc.expectPVID != "" { + assert.Equal(t, tc.expectPVID, route.ProviderId) + } + if tc.expectCIDR != "" { + assert.Equal(t, tc.expectCIDR, route.DestinationCIDR) + } + } else { + assert.Nil(t, route) + } + }) + } +} + +func TestBatchDeleteRoutes(t *testing.T) { + cloud := vmock.MockCloud{ + MockVPC: vmock.NewMockVPC(nil), + IMetaData: vmock.NewMockMetaData("vpc-single-route-table"), + } + eventRecord := record.NewFakeRecorder(100) + rateLimiter := workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 10*time.Second) + nodeCache := cmap.New() + requeueChan := make(chan event.GenericEvent, 10) + + recon := &ReconcileRoute{ + cloud: cloud, + record: eventRecord, + nodeCache: nodeCache, + rateLimiter: rateLimiter, + requeueChan: requeueChan, + } + + ctx := context.Background() + reconcileID := "test-reconcile-id" + table := "table-1" + + t.Run("empty routes list", func(t *testing.T) { + err := recon.batchDeleteRoutes(ctx, reconcileID, table, []*model.Route{}) + assert.NoError(t, err) + }) + + t.Run("successful delete", func(t *testing.T) { + nodeRef := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + } + routes := []*model.Route{ + { + Name: "route-1", + DestinationCIDR: "192.168.1.0/24", + ProviderId: "i-123", + NodeReference: nodeRef, + }, + } + nodeCache.Set("node-1", routes[0]) + err := recon.batchDeleteRoutes(ctx, reconcileID, table, routes) + assert.NoError(t, err) + assert.False(t, nodeCache.Has("node-1")) + }) + + t.Run("route not exist ignore", func(t *testing.T) { + nodeRef := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-not-exist"}} + routes := []*model.Route{ + {ProviderId: "i-delete-not-exist", NodeReference: nodeRef}, + } + go func() { <-requeueChan }() + err := recon.batchDeleteRoutes(ctx, reconcileID, table, routes) + assert.NoError(t, err) + }) + + t.Run("delete failed requeue", func(t *testing.T) { + nodeRef := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-fail"}} + routes := []*model.Route{ + {ProviderId: "i-delete-fail", NodeReference: nodeRef}, + } + go func() { <-requeueChan }() + err := recon.batchDeleteRoutes(ctx, reconcileID, table, routes) + assert.NoError(t, err) + }) +} diff --git a/pkg/controller/route/predicate.go b/pkg/controller/route/predicate.go index 6a6d6aaa8..dfe1b93f8 100644 --- a/pkg/controller/route/predicate.go +++ b/pkg/controller/route/predicate.go @@ -35,7 +35,7 @@ func (sp *predicateForNodeEvent) Update(e event.UpdateEvent) bool { klog.Infof("node changed: %s Pod CIDR Changed: %v - %v", oldNode.Name, oldNode.Spec.PodCIDR, newNode.Spec.PodCIDR) return true } - if !reflect.DeepEqual(oldNode.Spec.PodCIDRs, oldNode.Spec.PodCIDRs) { + if !reflect.DeepEqual(oldNode.Spec.PodCIDRs, newNode.Spec.PodCIDRs) { klog.Infof("node changed: %s Pod CIDRs Changed: %v - %v", oldNode.Name, oldNode.Spec.PodCIDRs, newNode.Spec.PodCIDRs) return true } diff --git a/pkg/controller/route/predicate_test.go b/pkg/controller/route/predicate_test.go new file mode 100644 index 000000000..1ce64b0e1 --- /dev/null +++ b/pkg/controller/route/predicate_test.go @@ -0,0 +1,207 @@ +package route + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +func TestPredicateForNodeEvent_Create(t *testing.T) { + predicate := &predicateForNodeEvent{} + + t.Run("node with empty PodCIDR", func(t *testing.T) { + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "", // Empty PodCIDR + }, + } + + createEvent := event.CreateEvent{ + Object: node, + } + + result := predicate.Create(createEvent) + assert.False(t, result, "Expected Create to return false for node with empty PodCIDR") + }) + + t.Run("node with valid PodCIDR", func(t *testing.T) { + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.0.0/24", + }, + } + + createEvent := event.CreateEvent{ + Object: node, + } + + result := predicate.Create(createEvent) + assert.True(t, result, "Expected Create to return true for node with valid PodCIDR") + }) + + t.Run("non-node object", func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + } + + createEvent := event.CreateEvent{ + Object: pod, + } + + result := predicate.Create(createEvent) + assert.True(t, result, "Expected Create to return true for non-node objects") + }) +} + +func TestPredicateForNodeEvent_Update(t *testing.T) { + predicate := &predicateForNodeEvent{} + + t.Run("node UID changed", func(t *testing.T) { + oldNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + UID: "uid-1", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.0.0/24", + }, + } + + newNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + UID: "uid-2", // Changed UID + }, + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.0.0/24", + }, + } + + updateEvent := event.UpdateEvent{ + ObjectOld: oldNode, + ObjectNew: newNode, + } + + result := predicate.Update(updateEvent) + assert.True(t, result, "Expected Update to return true when node UID changes") + }) + + t.Run("node PodCIDR changed", func(t *testing.T) { + oldNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + UID: "uid-1", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.0.0/24", + }, + } + + newNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + UID: "uid-1", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.1.0/24", // Changed PodCIDR + }, + } + + updateEvent := event.UpdateEvent{ + ObjectOld: oldNode, + ObjectNew: newNode, + } + + result := predicate.Update(updateEvent) + assert.True(t, result, "Expected Update to return true when node PodCIDR changes") + }) + + t.Run("node PodCIDRs changed", func(t *testing.T) { + oldNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + UID: "uid-1", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.0.0/24", + PodCIDRs: []string{"10.0.0.0/24"}, + }, + } + + newNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + UID: "uid-1", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.0.0/24", + PodCIDRs: []string{"10.0.1.0/24"}, // Changed PodCIDRs + }, + } + + updateEvent := event.UpdateEvent{ + ObjectOld: oldNode, + ObjectNew: newNode, + } + + result := predicate.Update(updateEvent) + assert.True(t, result, "Expected Update to return true when node PodCIDRs changes") + }) + + t.Run("node unchanged", func(t *testing.T) { + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + UID: "uid-1", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.0.0/24", + PodCIDRs: []string{"10.0.0.0/24"}, + }, + } + + updateEvent := event.UpdateEvent{ + ObjectOld: node, + ObjectNew: node, + } + + result := predicate.Update(updateEvent) + assert.False(t, result, "Expected Update to return false when node is unchanged") + }) + + t.Run("non-node objects", func(t *testing.T) { + oldPod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "old-pod", + Namespace: "default", + }, + } + + newPod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-pod", + Namespace: "default", + }, + } + + updateEvent := event.UpdateEvent{ + ObjectOld: oldPod, + ObjectNew: newPod, + } + + result := predicate.Update(updateEvent) + assert.True(t, result, "Expected Update to return true for non-node objects") + }) +} diff --git a/pkg/controller/route/route_controller.go b/pkg/controller/route/route_controller.go index ab7dd361f..72a672e42 100644 --- a/pkg/controller/route/route_controller.go +++ b/pkg/controller/route/route_controller.go @@ -85,7 +85,6 @@ func Add(mgr manager.Manager, ctx *shared.SharedContext) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager, ctx *shared.SharedContext, requeue chan<- event.GenericEvent, rateLimiter workqueue.RateLimiter) *ReconcileRoute { - recon := &ReconcileRoute{ cloud: ctx.Provider(), client: mgr.GetClient(), @@ -109,7 +108,7 @@ type routeController struct { // Start function will not be called until the resource lock is acquired func (controller routeController) Start(ctx context.Context) error { if controller.recon.configRoutes { - controller.recon.periodicalSync() + controller.recon.periodicalSync(ctx) for i := range ctrlCfg.CloudCFG.Global.RouteMaxConcurrentReconciles { go controller.recon.batchWorker(ctx, i) } @@ -142,7 +141,7 @@ type ReconcileRoute struct { } func (r *ReconcileRoute) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { - // if do not need route, skip all node events + // if it's not needed to create routes, skip it if !r.configRoutes { return reconcile.Result{}, nil } @@ -215,45 +214,6 @@ outer: } } -func (r *ReconcileRoute) syncCloudRoute(ctx context.Context, node *corev1.Node) error { - if !needSyncRoute(node) { - return nil - } - - prvdId := node.Spec.ProviderID - if prvdId == "" { - klog.Warningf("node %s provider id is not exist, skip creating route", node.Name) - return nil - } - - _, ipv4RouteCidr, err := getIPv4RouteForNode(node) - if err != nil || ipv4RouteCidr == "" { - klog.Warningf("node %s parse podCIDR %s error, skip creating route", node.Name, node.Spec.PodCIDR) - if err1 := r.updateNetworkingCondition(ctx, node, false); err1 != nil { - klog.Errorf("route, update network condition error: %v", err1) - } - return err - } - - tables, err := getRouteTables(ctx, r.cloud) - if err != nil { - return err - } - var tablesErr []error - for _, table := range tables { - tablesErr = append(tablesErr, r.addRouteForNode(ctx, table, ipv4RouteCidr, prvdId, node, nil)) - } - if utilerrors.NewAggregate(tablesErr) != nil { - err := r.updateNetworkingCondition(ctx, node, false) - if err != nil { - klog.Errorf("update network condition for node %s, error: %v", node.Name, err.Error()) - } - return utilerrors.NewAggregate(tablesErr) - } else { - return r.updateNetworkingCondition(ctx, node, true) - } -} - func (r *ReconcileRoute) addRouteForNode( ctx context.Context, table, ipv4Cidr, prvdId string, node *corev1.Node, cachedRouteEntry []*model.Route, ) error { @@ -358,10 +318,10 @@ func (r *ReconcileRoute) updateNetworkingCondition(ctx context.Context, node *co return err } -func (r *ReconcileRoute) periodicalSync() { +func (r *ReconcileRoute) periodicalSync(ctx context.Context) { go func() { time.Sleep(r.reconcilePeriod) - wait.Until(r.reconcileForCluster, r.reconcilePeriod, wait.NeverStop) + wait.Until(r.reconcileForCluster, r.reconcilePeriod, ctx.Done()) }() } diff --git a/pkg/controller/route/route_controller_test.go b/pkg/controller/route/route_controller_test.go index 06b283fde..5df494215 100644 --- a/pkg/controller/route/route_controller_test.go +++ b/pkg/controller/route/route_controller_test.go @@ -2,18 +2,26 @@ package route import ( "context" + "testing" + "time" + cmap "github.com/orcaman/concurrent-map" + "golang.org/x/time/rate" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + ctrlCfg "k8s.io/cloud-provider-alibaba-cloud/pkg/config" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model" prvd "k8s.io/cloud-provider-alibaba-cloud/pkg/provider" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" "k8s.io/cloud-provider-alibaba-cloud/pkg/util" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) var ( @@ -22,44 +30,568 @@ var ( func TestUpdateNetworkingCondition(t *testing.T) { r := getReconcileRoute() - nodes := &v1.NodeList{} - err := r.client.List(context.TODO(), nodes) if err != nil { - panic(err) + t.Fatal(err) } - for _, node := range nodes.Items { - err := r.updateNetworkingCondition(context.TODO(), &node, true) + t.Run("routeCreated true patch success", func(t *testing.T) { + for i := range nodes.Items { + node := &nodes.Items[i] + err := r.updateNetworkingCondition(context.TODO(), node, true) + if err != nil { + t.Error(err) + } + updatedNode := &v1.Node{} + err = r.client.Get(context.TODO(), util.NamespacedName(node), updatedNode) + if err != nil { + t.Error(err) + } + networkCondition, ok := helper.FindCondition(updatedNode.Status.Conditions, v1.NodeNetworkUnavailable) + if !ok || networkCondition.Status != v1.ConditionFalse { + t.Error("node condition update failed") + } + } + }) + + t.Run("routeCreated true already ConditionFalse early return", func(t *testing.T) { + var nodeWithFalse *v1.Node + for i := range nodes.Items { + if nodes.Items[i].Status.Conditions != nil { + for _, c := range nodes.Items[i].Status.Conditions { + if c.Type == v1.NodeNetworkUnavailable && c.Status == v1.ConditionFalse { + nodeWithFalse = &nodes.Items[i] + break + } + } + } + if nodeWithFalse != nil { + break + } + } + if nodeWithFalse == nil { + t.Skip("no node with NodeNetworkUnavailable=False in fixture") + } + err := r.updateNetworkingCondition(context.TODO(), nodeWithFalse, true) if err != nil { t.Error(err) } + }) - updatedNode := &v1.Node{} - err = r.client.Get(context.TODO(), util.NamespacedName(&node), updatedNode) + t.Run("routeCreated false already ConditionTrue early return", func(t *testing.T) { + var nodeWithTrue *v1.Node + for i := range nodes.Items { + if nodes.Items[i].Name == NodeName { + nodeWithTrue = &nodes.Items[i] + break + } + } + if nodeWithTrue == nil { + t.Fatal("fixture node not found") + } + err := r.updateNetworkingCondition(context.TODO(), nodeWithTrue, false) if err != nil { t.Error(err) } + }) + t.Run("routeCreated false patch to True", func(t *testing.T) { + var nodeWithFalse *v1.Node + for i := range nodes.Items { + for _, c := range nodes.Items[i].Status.Conditions { + if c.Type == v1.NodeNetworkUnavailable && c.Status == v1.ConditionFalse { + nodeWithFalse = &nodes.Items[i] + break + } + } + if nodeWithFalse != nil { + break + } + } + if nodeWithFalse == nil { + t.Skip("no node with NodeNetworkUnavailable=False") + } + err := r.updateNetworkingCondition(context.TODO(), nodeWithFalse, false) + if err != nil { + t.Error(err) + } + updatedNode := &v1.Node{} + err = r.client.Get(context.TODO(), util.NamespacedName(nodeWithFalse), updatedNode) + if err != nil { + t.Error(err) + } networkCondition, ok := helper.FindCondition(updatedNode.Status.Conditions, v1.NodeNetworkUnavailable) - if !ok || networkCondition.Status != v1.ConditionFalse { - t.Error("node condition update failed") + if !ok || networkCondition.Status != v1.ConditionTrue { + t.Error("expected ConditionTrue after routeCreated false") } + }) +} + +func TestSyncTableRoutes(t *testing.T) { + oldCIDR := ctrlCfg.ControllerCFG.ClusterCIDR + defer func() { ctrlCfg.ControllerCFG.ClusterCIDR = oldCIDR }() + ctrlCfg.ControllerCFG.ClusterCIDR = "10.96.0.0/16" + + r := getReconcileRoute() + nodes := &v1.NodeList{} + err := r.client.List(context.TODO(), nodes) + if err != nil { + t.Fatal(err) } + err = r.syncTableRoutes(context.Background(), "route-table-1", nodes) + if err != nil { + t.Error(err) + } + + t.Run("ListRoute error", func(t *testing.T) { + err := r.syncTableRoutes(context.Background(), "route-table-list-err", nodes) + if err == nil { + t.Error("expected error from ListRoute") + } + }) + + t.Run("invalid ClusterCIDR parse error", func(t *testing.T) { + ctrlCfg.ControllerCFG.ClusterCIDR = "invalid" + defer func() { ctrlCfg.ControllerCFG.ClusterCIDR = "10.96.0.0/16" }() + err := r.syncTableRoutes(context.Background(), "route-table-1", nodes) + if err == nil { + t.Error("expected parse error for invalid ClusterCIDR") + } + }) + + t.Run("node with needSyncRoute false skipped", func(t *testing.T) { + excludedNode := v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "excluded-node", + Labels: map[string]string{ + helper.LabelNodeExcludeNode: "true", + }, + }, + Spec: v1.NodeSpec{ProviderID: "cn-hangzhou.i-123", PodCIDR: "10.96.0.0/24"}, + } + customNodes := &v1.NodeList{Items: append(nodes.Items, excludedNode)} + err := r.syncTableRoutes(context.Background(), "route-table-1", customNodes) + if err != nil { + t.Error(err) + } + }) + + t.Run("node with empty ProviderID skipped", func(t *testing.T) { + noProviderNode := v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "no-provider-node"}, + Spec: v1.NodeSpec{ProviderID: "", PodCIDR: "10.96.0.0/24"}, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{{Type: v1.NodeReady, Status: v1.ConditionTrue}}, + }, + } + customNodes := &v1.NodeList{Items: append(nodes.Items, noProviderNode)} + err := r.syncTableRoutes(context.Background(), "route-table-1", customNodes) + if err != nil { + t.Error(err) + } + }) + + t.Run("empty ClusterCIDR", func(t *testing.T) { + oldCIDR := ctrlCfg.ControllerCFG.ClusterCIDR + ctrlCfg.ControllerCFG.ClusterCIDR = "" + defer func() { ctrlCfg.ControllerCFG.ClusterCIDR = oldCIDR }() + err := r.syncTableRoutes(context.Background(), "route-table-1", nodes) + if err != nil { + t.Error(err) + } + }) + + t.Run("route with invalid DestinationCIDR", func(t *testing.T) { + err := r.syncTableRoutes(context.Background(), "route-table-invalid-cidr", nodes) + if err != nil { + t.Error(err) + } + }) +} + +func TestAddRouteForNode(t *testing.T) { + r := getReconcileRoute() + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "test-node"}, + Spec: v1.NodeSpec{ProviderID: "cn-hangzhou.ecs-id", PodCIDR: "10.96.0.64/26"}, + } + err := r.addRouteForNode(context.Background(), "route-table-1", "10.96.0.64/26", "cn-hangzhou.ecs-id", node, nil) + if err != nil { + t.Error(err) + } + + err = r.addRouteForNode(context.Background(), "route-table-1", "", "", node, nil) + if err != nil { + t.Error(err) + } +} + +func TestReconcileRoute_Reconcile(t *testing.T) { + t.Run("configRoutes disabled", func(t *testing.T) { + r := getReconcileRoute() + r.configRoutes = false + + request := reconcile.Request{ + NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: NodeName}}), + } + + result, err := r.Reconcile(context.TODO(), request) + if err != nil { + t.Errorf("Reconcile failed: %v", err) + } + + if result != (reconcile.Result{}) { + t.Errorf("Expected empty result, got: %v", result) + } + + select { + case <-r.requestChan: + t.Error("Expected no message in requestChan when configRoutes is false") + default: + // Expected behavior - channel should be empty + } + }) + + t.Run("configRoutes enabled", func(t *testing.T) { + r := getReconcileRoute() + r.configRoutes = true + + request := reconcile.Request{ + NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: NodeName}}), + } + + result, err := r.Reconcile(context.TODO(), request) + if err != nil { + t.Errorf("Reconcile failed: %v", err) + } + + if result != (reconcile.Result{}) { + t.Errorf("Expected empty result, got: %v", result) + } + + select { + case receivedRequest := <-r.requestChan: + if receivedRequest != request { + t.Errorf("Expected request %v, got: %v", request, receivedRequest) + } + case <-time.After(1 * time.Second): + t.Error("Expected message in requestChan but timed out") + } + }) +} + +func TestBatchAddRoutes(t *testing.T) { + r := getReconcileRoute() + ctx := context.Background() + nodes := &v1.NodeList{} + if err := r.client.List(ctx, nodes); err != nil { + t.Fatal(err) + } + nodeByName := make(map[string]*v1.Node) + for i := range nodes.Items { + nodeByName[nodes.Items[i].Name] = &nodes.Items[i] + } + makeRoute := func(name, cidr, providerID string, node *v1.Node) *model.Route { + return &model.Route{ + Name: name, + DestinationCIDR: cidr, + ProviderId: providerID, + NodeReference: node, + } + } + + t.Run("empty routes", func(t *testing.T) { + err := r.batchAddRoutes(ctx, "rid", "route-table-1", nil) + if err != nil { + t.Error(err) + } + }) + + t.Run("duplicate and status_error treated as success", func(t *testing.T) { + dupNode := nodeByName["dup-cidr"] + statusNode := nodeByName["status-err"] + if dupNode == nil || statusNode == nil { + t.Skip("fixture nodes not found") + } + routes := []*model.Route{ + makeRoute("r1", "10.96.0.192/26", "i-dup", dupNode), + makeRoute("r2", "10.96.0.0/28", "i-se", statusNode), + } + err := r.batchAddRoutes(ctx, "rid", "route-table-1", routes) + if err != nil { + t.Error(err) + } + }) + + t.Run("create failed requeue", func(t *testing.T) { + cfNode := nodeByName["create-fail"] + if cfNode == nil { + t.Skip("create-fail node not found") + } + routes := []*model.Route{ + makeRoute("r-cf", "10.96.0.16/28", "i-cf", cfNode), + } + err := r.batchAddRoutes(ctx, "rid", "route-table-1", routes) + if err != nil { + t.Error(err) + } + }) +} + +func TestReconcileRoute_BatchSyncCloudRoutes(t *testing.T) { + t.Run("no nodes to process", func(t *testing.T) { + r := getReconcileRoute() + + // Test empty request list + err := r.batchSyncCloudRoutes(context.Background(), "test-reconcile-id", []reconcile.Request{}) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("node not found", func(t *testing.T) { + r := getReconcileRoute() + requests := []reconcile.Request{ + { + NamespacedName: util.NamespacedName(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "non-existent-node"}, + }), + }, + } + + err := r.batchSyncCloudRoutes(context.Background(), "test-reconcile-id", requests) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("node not found with cache entry", func(t *testing.T) { + r := getReconcileRoute() + r.nodeCache.Set("deleted-node", &model.Route{ + Name: "r-deleted", + DestinationCIDR: "10.96.0.0/24", + ProviderId: "i-deleted", + NodeReference: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "deleted-node"}}, + }) + requests := []reconcile.Request{ + {NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "deleted-node"}})}, + } + err := r.batchSyncCloudRoutes(context.Background(), "rid", requests) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("node with no provider id", func(t *testing.T) { + r := getReconcileRoute() + requests := []reconcile.Request{ + { + NamespacedName: util.NamespacedName(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "cn-hangzhou.192.0.168.69"}, + }), + }, + } + node := &v1.Node{} + err := r.client.Get(context.Background(), util.NamespacedName(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "cn-hangzhou.192.0.168.69"}, + }), node) + if err != nil { + t.Fatalf("Failed to get node: %v", err) + } + + node.Spec.ProviderID = "" + err = r.client.Update(context.Background(), node) + if err != nil { + t.Fatalf("Failed to update node: %v", err) + } + + err = r.batchSyncCloudRoutes(context.Background(), "test-reconcile-id", requests) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + t.Run("invalid providerID requeue", func(t *testing.T) { + r := getReconcileRoute() + requests := []reconcile.Request{ + {NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "invalid-provider"}})}, + } + err := r.batchSyncCloudRoutes(context.Background(), "rid", requests) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("bad PodCIDR update condition and requeue", func(t *testing.T) { + r := getReconcileRoute() + requests := []reconcile.Request{ + {NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "bad-podcidr"}})}, + } + err := r.batchSyncCloudRoutes(context.Background(), "rid", requests) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("getRouteTables error", func(t *testing.T) { + cloudNoTable := vmock.MockCloud{ + MockVPC: vmock.NewMockVPC(nil), + IMetaData: vmock.NewMockMetaData("vpc-no-route-table"), + } + oldRouteTableIDS := ctrlCfg.CloudCFG.Global.RouteTableIDS + ctrlCfg.CloudCFG.Global.RouteTableIDS = "" + defer func() { ctrlCfg.CloudCFG.Global.RouteTableIDS = oldRouteTableIDS }() + r := getReconcileRouteWithCloud(cloudNoTable) + requests := []reconcile.Request{ + {NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: NodeName}})}, + } + err := r.batchSyncCloudRoutes(context.Background(), "rid", requests) + if err == nil { + t.Error("expected getRouteTables error") + } + }) +} + +func TestReconcileRoute_ReconcileForCluster(t *testing.T) { + t.Run("reconcileForCluster success", func(t *testing.T) { + r := getReconcileRoute() + r.reconcileForCluster() + }) + + t.Run("reconcileForCluster getRouteTables error", func(t *testing.T) { + cloudNoTable := vmock.MockCloud{ + MockVPC: vmock.NewMockVPC(nil), + IMetaData: vmock.NewMockMetaData("vpc-no-route-table"), + } + oldRouteTableIDS := ctrlCfg.CloudCFG.Global.RouteTableIDS + ctrlCfg.CloudCFG.Global.RouteTableIDS = "" + defer func() { ctrlCfg.CloudCFG.Global.RouteTableIDS = oldRouteTableIDS }() + r := getReconcileRouteWithCloud(cloudNoTable) + r.reconcileForCluster() + }) + + t.Run("reconcileForCluster syncTableRoutes error", func(t *testing.T) { + oldRouteTableIDS := ctrlCfg.CloudCFG.Global.RouteTableIDS + ctrlCfg.CloudCFG.Global.RouteTableIDS = "route-table-list-err" + defer func() { ctrlCfg.CloudCFG.Global.RouteTableIDS = oldRouteTableIDS }() + r := getReconcileRoute() + r.reconcileForCluster() + }) +} + +func TestReconcileRoute_BatchWorker(t *testing.T) { + ctrlCfg.ControllerCFG.RouteReconcileBatchSize = 10 + + t.Run("batchWorker context cancel", func(t *testing.T) { + r := getReconcileRoute() + + // Create a cancellable context + ctx, cancel := context.WithCancel(context.Background()) + + // Start batchWorker + go r.batchWorker(ctx, 0) + + // Cancel context immediately + cancel() + + // Wait a short time to ensure goroutine has time to process cancel signal + time.Sleep(100 * time.Millisecond) + + // Test passes if no panic occurs + }) + + t.Run("batchWorker process requests", func(t *testing.T) { + r := getReconcileRoute() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go r.batchWorker(ctx, 1) + + request := reconcile.Request{ + NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: NodeName}}), + } + r.requestChan <- request + + time.Sleep(2 * time.Second) + }) + + t.Run("batchWorker handle multiple requests", func(t *testing.T) { + r := getReconcileRoute() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go r.batchWorker(ctx, 2) + request1 := reconcile.Request{ + NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: NodeName}}), + } + request2 := reconcile.Request{ + NamespacedName: util.NamespacedName(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "cn-hangzhou.192.0.168.69"}}), + } + + r.requestChan <- request1 + r.requestChan <- request2 + r.requestChan <- request1 // Duplicate request + + time.Sleep(2 * time.Second) + }) +} + +func TestRequeueNode(t *testing.T) { + t.Run("requeue sent", func(t *testing.T) { + requeueCh := make(chan event.GenericEvent, 2) + r := &ReconcileRoute{ + cloud: getMockCloudProvider(), + client: getFakeKubeClient(), + record: record.NewFakeRecorder(10), + nodeCache: cmap.New(), + requeueChan: requeueCh, + requestChan: make(chan reconcile.Request, 10), + } + n := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node"}} + r.requeueNode(n) + ev := <-requeueCh + if ev.Object.GetName() != "test-node" { + t.Errorf("unexpected node name: %s", ev.Object.GetName()) + } + }) + + t.Run("requeue channel full", func(t *testing.T) { + requeueCh := make(chan event.GenericEvent, 1) + r := &ReconcileRoute{ + cloud: getMockCloudProvider(), + client: getFakeKubeClient(), + record: record.NewFakeRecorder(10), + nodeCache: cmap.New(), + requeueChan: requeueCh, + requestChan: make(chan reconcile.Request, 10), + } + requeueCh <- event.GenericEvent{Object: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "blocking"}}} + n := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "dropped"}} + r.requeueNode(n) + ev := <-requeueCh + if ev.Object.GetName() != "blocking" { + t.Errorf("expected blocking, got %s", ev.Object.GetName()) + } + }) } func getReconcileRoute() *ReconcileRoute { eventRecord := record.NewFakeRecorder(100) + requeueCh := make(chan event.GenericEvent, 10) recon := &ReconcileRoute{ cloud: getMockCloudProvider(), client: getFakeKubeClient(), record: eventRecord, nodeCache: cmap.New(), configRoutes: true, + requeueChan: requeueCh, + rateLimiter: workqueue.NewMaxOfRateLimiter( + workqueue.NewItemExponentialFailureRateLimiter(5*time.Second, 300*time.Second), + &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)}, + ), + requestChan: make(chan reconcile.Request, 10), } - return recon } @@ -70,8 +602,25 @@ func getMockCloudProvider() prvd.Provider { } } +func getReconcileRouteWithCloud(cloud prvd.Provider) *ReconcileRoute { + eventRecord := record.NewFakeRecorder(100) + requeueCh := make(chan event.GenericEvent, 10) + return &ReconcileRoute{ + cloud: cloud, + client: getFakeKubeClient(), + record: eventRecord, + nodeCache: cmap.New(), + configRoutes: true, + requeueChan: requeueCh, + rateLimiter: workqueue.NewMaxOfRateLimiter( + workqueue.NewItemExponentialFailureRateLimiter(5*time.Second, 300*time.Second), + &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)}, + ), + requestChan: make(chan reconcile.Request, 10), + } +} + func getFakeKubeClient() client.Client { - // Node nodeList := &v1.NodeList{ Items: []v1.Node{ { @@ -120,6 +669,31 @@ func getFakeKubeClient() client.Client { }, }, }, + { + ObjectMeta: metav1.ObjectMeta{Name: "dup-cidr"}, + Spec: v1.NodeSpec{ProviderID: "alicloud://i-dup", PodCIDR: "10.96.0.192/26"}, + Status: v1.NodeStatus{Conditions: []v1.NodeCondition{}}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "status-err"}, + Spec: v1.NodeSpec{ProviderID: "alicloud://i-se", PodCIDR: "10.96.0.0/28"}, + Status: v1.NodeStatus{Conditions: []v1.NodeCondition{}}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "create-fail"}, + Spec: v1.NodeSpec{ProviderID: "alicloud://i-cf", PodCIDR: "10.96.0.16/28"}, + Status: v1.NodeStatus{Conditions: []v1.NodeCondition{}}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "invalid-provider"}, + Spec: v1.NodeSpec{ProviderID: "x", PodCIDR: "10.96.0.32/28"}, + Status: v1.NodeStatus{Conditions: []v1.NodeCondition{{Type: v1.NodeReady, Status: v1.ConditionTrue}}}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "bad-podcidr"}, + Spec: v1.NodeSpec{ProviderID: "alicloud://cn-hangzhou.i-bad", PodCIDR: ""}, + Status: v1.NodeStatus{Conditions: []v1.NodeCondition{{Type: v1.NodeReady, Status: v1.ConditionTrue}}}, + }, }, } diff --git a/pkg/controller/service/clbv1/coverage b/pkg/controller/service/clbv1/coverage new file mode 100644 index 000000000..5f02b1119 --- /dev/null +++ b/pkg/controller/service/clbv1/coverage @@ -0,0 +1 @@ +mode: set diff --git a/pkg/controller/service/clbv1/event_handler_test.go b/pkg/controller/service/clbv1/event_handler_test.go index 3d4f2f653..76d5eb913 100644 --- a/pkg/controller/service/clbv1/event_handler_test.go +++ b/pkg/controller/service/clbv1/event_handler_test.go @@ -2,19 +2,24 @@ package clbv1 import ( "context" + "testing" + "time" + "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apiserver/pkg/util/feature" + "k8s.io/client-go/tools/leaderelection/resourcelock" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" + "k8s.io/cloud-provider-alibaba-cloud/pkg/config" helper "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" + "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/event" - "testing" - "time" ) func TestEnqueueRequestForServiceEvent(t *testing.T) { @@ -79,6 +84,11 @@ func TestNeedAdd(t *testing.T) { clusterIPSvc := svc.DeepCopy() clusterIPSvc.Spec.Type = v1.ServiceTypeClusterIP assert.Equal(t, needAdd(clusterIPSvc), false) + + tunnelSvc := svc.DeepCopy() + tunnelSvc.Spec.Type = v1.ServiceTypeClusterIP + tunnelSvc.Annotations = map[string]string{helper.TunnelType: "tunnel"} + assert.Equal(t, needAdd(tunnelSvc), false) } func TestEnqueueRequestForEndpointEvent(t *testing.T) { @@ -109,7 +119,36 @@ func TestIsEndpointProcessNeeded(t *testing.T) { }, ep); err != nil { t.Error(err) } - assert.Equal(t, isEndpointProcessNeeded(ep, cl), true) + assert.True(t, isEndpointProcessNeeded(ep, cl)) + + assert.False(t, isEndpointProcessNeeded(nil, cl)) + + epLeader := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: NS, + Name: SvcName, + Annotations: map[string]string{ + resourcelock.LeaderElectionRecordAnnotationKey: "{}", + }, + }, + } + assert.False(t, isEndpointProcessNeeded(epLeader, cl)) + + epNoSvc := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{Namespace: NS, Name: "no-such-svc"}, + } + clNoSvc := fake.NewClientBuilder().WithRuntimeObjects(epNoSvc).Build() + assert.False(t, isEndpointProcessNeeded(epNoSvc, clNoSvc)) + + clusterIPSvc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "clusterip-svc", Namespace: NS}, + Spec: v1.ServiceSpec{Type: v1.ServiceTypeClusterIP}, + } + epClusterIP := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{Name: "clusterip-svc", Namespace: NS}, + } + clClusterIP := fake.NewClientBuilder().WithRuntimeObjects(clusterIPSvc, epClusterIP).Build() + assert.False(t, isEndpointProcessNeeded(epClusterIP, clClusterIP)) } func TestEnqueueRequestForNodeEvent(t *testing.T) { @@ -207,7 +246,38 @@ func TestIsEndpointSliceProcessNeeded(t *testing.T) { }, es); err != nil { t.Error(err) } - assert.Equal(t, isEndpointSliceProcessNeeded(es, cl), true) + assert.True(t, isEndpointSliceProcessNeeded(es, cl)) + + assert.False(t, isEndpointSliceProcessNeeded(nil, cl)) + + esNoLabel := &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{Name: "no-label", Namespace: NS, Labels: map[string]string{}}, + } + assert.False(t, isEndpointSliceProcessNeeded(esNoLabel, cl)) + + esNoSvc := &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-such-svc-es", + Namespace: NS, + Labels: map[string]string{discovery.LabelServiceName: "no-such-svc"}, + }, + } + clNoSvc := fake.NewClientBuilder().WithRuntimeObjects(esNoSvc).Build() + assert.False(t, isEndpointSliceProcessNeeded(esNoSvc, clNoSvc)) + + clusterIPSvc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "clusterip-svc", Namespace: NS}, + Spec: v1.ServiceSpec{Type: v1.ServiceTypeClusterIP}, + } + esClusterIP := &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterip-svc-es", + Namespace: NS, + Labels: map[string]string{discovery.LabelServiceName: "clusterip-svc"}, + }, + } + clClusterIP := fake.NewClientBuilder().WithRuntimeObjects(clusterIPSvc, esClusterIP).Build() + assert.False(t, isEndpointSliceProcessNeeded(esClusterIP, clClusterIP)) } func TestIsEndpointSliceUpdateNeeded(t *testing.T) { @@ -225,3 +295,181 @@ func TestIsEndpointSliceUpdateNeeded(t *testing.T) { newES.Ports[0].Port = &newPort assert.Equal(t, isEndpointSliceUpdateNeeded(oldES, newES), true) } + +func TestCheckServiceAffected(t *testing.T) { + cl := getFakeKubeClient() + h := NewEnqueueRequestForNodeEvent(cl, record.NewFakeRecorder(100)) + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: NodeName, + }, + } + + t.Run("ENI backend type", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[helper.BackendType] = "eni" + result := h.checkServiceAffected(node, svc) + assert.False(t, result) + }) + + t.Run("feature gate disabled", func(t *testing.T) { + defaultEnabled := feature.DefaultMutableFeatureGate.Enabled(config.FilterServiceOnNodeChange) + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): false, + }) + assert.NoError(t, err) + defer func() { + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): defaultEnabled, + }) + assert.NoError(t, err) + }() + + svc := getDefaultService() + result := h.checkServiceAffected(node, svc) + assert.True(t, result) + }) + + t.Run("cluster external traffic policy", func(t *testing.T) { + defaultEnabled := feature.DefaultMutableFeatureGate.Enabled(config.FilterServiceOnNodeChange) + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): true, + }) + assert.NoError(t, err) + defer func() { + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): defaultEnabled, + }) + assert.NoError(t, err) + }() + + svc := getDefaultService() + svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeCluster + result := h.checkServiceAffected(node, svc) + assert.True(t, result) + }) + + t.Run("with backend label annotation", func(t *testing.T) { + defaultEnabled := feature.DefaultMutableFeatureGate.Enabled(config.FilterServiceOnNodeChange) + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): true, + }) + assert.NoError(t, err) + defer func() { + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): defaultEnabled, + }) + assert.NoError(t, err) + }() + + svc := getDefaultService() + svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeLocal + svc.Annotations[annotation.Annotation(annotation.BackendLabel)] = "app=nginx" + result := h.checkServiceAffected(node, svc) + assert.True(t, result) + }) + + t.Run("with remove unscheduled annotation", func(t *testing.T) { + defaultEnabled := feature.DefaultMutableFeatureGate.Enabled(config.FilterServiceOnNodeChange) + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): true, + }) + assert.NoError(t, err) + defer func() { + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): defaultEnabled, + }) + assert.NoError(t, err) + }() + + svc := getDefaultService() + svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeLocal + svc.Annotations[annotation.Annotation(annotation.RemoveUnscheduled)] = "true" + result := h.checkServiceAffected(node, svc) + assert.True(t, result) + }) + + t.Run("not affected", func(t *testing.T) { + defaultEnabled := feature.DefaultMutableFeatureGate.Enabled(config.FilterServiceOnNodeChange) + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): true, + }) + assert.NoError(t, err) + defer func() { + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): defaultEnabled, + }) + assert.NoError(t, err) + }() + + svc := getDefaultService() + svc.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeLocal + result := h.checkServiceAffected(node, svc) + assert.False(t, result) + }) +} + +func TestCanNodeSkipEventHandler(t *testing.T) { + assert.False(t, canNodeSkipEventHandler(nil)) + nodeNoLabels := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "n"}} + nodeNoLabels.Labels = nil + assert.False(t, canNodeSkipEventHandler(nodeNoLabels)) + assert.False(t, canNodeSkipEventHandler(&v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "n", Labels: map[string]string{}}})) + + nodeExcluded := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "excluded", + Labels: map[string]string{helper.LabelNodeExcludeNode: "true"}, + }, + } + assert.True(t, canNodeSkipEventHandler(nodeExcluded)) + + nodeMaster := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "master", + Labels: map[string]string{ + "node-role.kubernetes.io/master": "", + }, + }, + } + assert.True(t, canNodeSkipEventHandler(nodeMaster)) +} + +func TestEventHandlerNoopBranches(t *testing.T) { + ctx := context.TODO() + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + defer queue.ShutDown() + + cl := getFakeKubeClient() + svcHandler := NewEnqueueRequestForServiceEvent(record.NewFakeRecorder(100)) + epHandler := NewEnqueueRequestForEndpointEvent(cl, record.NewFakeRecorder(100)) + nodeHandler := NewEnqueueRequestForNodeEvent(cl, record.NewFakeRecorder(100)) + esHandler := NewEnqueueRequestForEndpointSliceEvent(cl, record.NewFakeRecorder(100)) + + svc := getDefaultService() + ep := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: SvcName, + Namespace: NS, + }, + } + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: NodeName, + }, + } + es := &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: SvcName, + Namespace: NS, + }, + } + + svcHandler.Delete(ctx, event.DeleteEvent{Object: svc}, queue) + svcHandler.Generic(ctx, event.GenericEvent{Object: svc}, queue) + epHandler.Generic(ctx, event.GenericEvent{Object: ep}, queue) + nodeHandler.Generic(ctx, event.GenericEvent{Object: node}, queue) + esHandler.Generic(ctx, event.GenericEvent{Object: es}, queue) + + assert.Equal(t, 0, queue.Len()) +} diff --git a/pkg/controller/service/clbv1/listeners_test.go b/pkg/controller/service/clbv1/listeners_test.go index 4259504df..9e883b05d 100644 --- a/pkg/controller/service/clbv1/listeners_test.go +++ b/pkg/controller/service/clbv1/listeners_test.go @@ -1,9 +1,19 @@ package clbv1 import ( + "context" + "testing" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" + svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" "k8s.io/cloud-provider-alibaba-cloud/pkg/model" - "testing" + "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/alibaba/base" + "k8s.io/klog/v2/klogr" ) func TestIsListenerACLIDsEqual(t *testing.T) { @@ -83,3 +93,742 @@ func TestIsListenerACLIDsEqual(t *testing.T) { }) } } + +func TestForwardPort(t *testing.T) { + cases := []struct { + name string + port string + target int + expected int + expectError bool + }{ + { + name: "empty port", + port: "", + target: 80, + expected: 0, + expectError: true, + }, + { + name: "valid forward", + port: "80:443", + target: 80, + expected: 443, + expectError: false, + }, + { + name: "multiple forwards", + port: "80:443,8080:8443", + target: 8080, + expected: 8443, + expectError: false, + }, + { + name: "no match", + port: "80:443", + target: 8080, + expected: 0, + expectError: false, + }, + { + name: "invalid format", + port: "80", + target: 80, + expected: 0, + expectError: true, + }, + { + name: "invalid number", + port: "80:abc", + target: 80, + expected: 0, + expectError: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := forwardPort(c.port, c.target) + if c.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, c.expected, result) + } + }) + } +} + +func TestProtocol(t *testing.T) { + cases := []struct { + name string + annotation string + port v1.ServicePort + expected string + expectError bool + }{ + { + name: "no annotation", + annotation: "", + port: v1.ServicePort{Port: 80, Protocol: v1.ProtocolTCP}, + expected: "tcp", + expectError: false, + }, + { + name: "https protocol", + annotation: "https:443", + port: v1.ServicePort{Port: 443, Protocol: v1.ProtocolTCP}, + expected: "https", + expectError: false, + }, + { + name: "http protocol", + annotation: "http:80", + port: v1.ServicePort{Port: 80, Protocol: v1.ProtocolTCP}, + expected: "http", + expectError: false, + }, + { + name: "udp protocol", + annotation: "udp:53", + port: v1.ServicePort{Port: 53, Protocol: v1.ProtocolUDP}, + expected: "udp", + expectError: false, + }, + { + name: "multiple protocols", + annotation: "http:80,https:443", + port: v1.ServicePort{Port: 443, Protocol: v1.ProtocolTCP}, + expected: "https", + expectError: false, + }, + { + name: "invalid format", + annotation: "https", + port: v1.ServicePort{Port: 443, Protocol: v1.ProtocolTCP}, + expected: "", + expectError: true, + }, + { + name: "unsupported protocol", + annotation: "ftp:21", + port: v1.ServicePort{Port: 21, Protocol: v1.ProtocolTCP}, + expected: "", + expectError: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := protocol(c.annotation, c.port) + if c.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, c.expected, result) + } + }) + } +} + +func TestVgroup(t *testing.T) { + cases := []struct { + name string + annotation string + port v1.ServicePort + expected string + expectError bool + }{ + { + name: "valid vgroup", + annotation: "vsp-xxx:80", + port: v1.ServicePort{Port: 80}, + expected: "vsp-xxx", + expectError: false, + }, + { + name: "multiple vgroups", + annotation: "vsp-xxx:80,vsp-yyy:443", + port: v1.ServicePort{Port: 443}, + expected: "vsp-yyy", + expectError: false, + }, + { + name: "no match", + annotation: "vsp-xxx:80", + port: v1.ServicePort{Port: 443}, + expected: "", + expectError: false, + }, + { + name: "invalid format", + annotation: "vsp-xxx", + port: v1.ServicePort{Port: 80}, + expected: "", + expectError: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := vgroup(c.annotation, c.port) + if c.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, c.expected, result) + } + }) + } +} + +func TestSetDefaultValueForListener(t *testing.T) { + t.Run("set default scheduler", func(t *testing.T) { + listener := &model.ListenerAttribute{} + setDefaultValueForListener(listener) + assert.Equal(t, "rr", listener.Scheduler) + }) + + t.Run("keep existing scheduler", func(t *testing.T) { + listener := &model.ListenerAttribute{Scheduler: "wrr"} + setDefaultValueForListener(listener) + assert.Equal(t, "wrr", listener.Scheduler) + }) + + t.Run("set default bandwidth for tcp", func(t *testing.T) { + listener := &model.ListenerAttribute{Protocol: model.TCP} + setDefaultValueForListener(listener) + assert.Equal(t, DefaultListenerBandwidth, listener.Bandwidth) + }) + + t.Run("set default health check for http", func(t *testing.T) { + listener := &model.ListenerAttribute{Protocol: model.HTTP} + setDefaultValueForListener(listener) + assert.Equal(t, model.OffFlag, listener.HealthCheck) + assert.Equal(t, model.OffFlag, listener.StickySession) + }) +} + +func TestFindVServerGroup(t *testing.T) { + t.Run("found vgroup", func(t *testing.T) { + vgs := []model.VServerGroup{ + {VGroupId: "vsg-1", VGroupName: "group-1"}, + {VGroupId: "vsg-2", VGroupName: "group-2"}, + } + listener := &model.ListenerAttribute{VGroupName: "group-1"} + err := findVServerGroup(vgs, listener) + assert.NoError(t, err) + assert.Equal(t, "vsg-1", listener.VGroupId) + }) + + t.Run("not found vgroup", func(t *testing.T) { + vgs := []model.VServerGroup{ + {VGroupId: "vsg-1", VGroupName: "group-1"}, + } + listener := &model.ListenerAttribute{VGroupName: "group-2"} + err := findVServerGroup(vgs, listener) + assert.Error(t, err) + }) +} + +func TestIsPortManagedByMyService(t *testing.T) { + t.Run("managed by my service", func(t *testing.T) { + local := &model.LoadBalancer{ + NamespacedName: types.NamespacedName{Name: "test", Namespace: "default"}, + } + listener := model.ListenerAttribute{ + NamedKey: &model.ListenerNamedKey{ + ServiceName: "test", + Namespace: "default", + CID: base.CLUSTER_ID, + }, + } + result := isPortManagedByMyService(local, listener) + assert.True(t, result) + }) + + t.Run("not managed - user managed", func(t *testing.T) { + local := &model.LoadBalancer{ + NamespacedName: types.NamespacedName{Name: "test", Namespace: "default"}, + } + listener := model.ListenerAttribute{ + IsUserManaged: true, + NamedKey: &model.ListenerNamedKey{ + ServiceName: "test", + Namespace: "default", + CID: base.CLUSTER_ID, + }, + } + result := isPortManagedByMyService(local, listener) + assert.False(t, result) + }) + + t.Run("not managed - different service", func(t *testing.T) { + local := &model.LoadBalancer{ + NamespacedName: types.NamespacedName{Name: "test", Namespace: "default"}, + } + listener := model.ListenerAttribute{ + NamedKey: &model.ListenerNamedKey{ + ServiceName: "other", + Namespace: "default", + CID: base.CLUSTER_ID, + }, + } + result := isPortManagedByMyService(local, listener) + assert.False(t, result) + }) +} + +func TestGetVGroupNamedKey(t *testing.T) { + t.Run("ecs backend", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + port := v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + NodePort: 30080, + } + key := getVGroupNamedKey(svc, port) + assert.Equal(t, "default", key.Namespace) + assert.Equal(t, "test", key.ServiceName) + assert.Equal(t, "30080", key.VGroupPort) + }) + + t.Run("eni backend with int target port", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Annotations: map[string]string{annotation.BackendType: model.ENIBackendType}, + }, + } + port := v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + NodePort: 30080, + } + key := getVGroupNamedKey(svc, port) + assert.Equal(t, "8080", key.VGroupPort) + }) + + t.Run("eni backend with string target port", func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Annotations: map[string]string{annotation.BackendType: model.ENIBackendType}, + }, + } + port := v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromString("http"), + NodePort: 30080, + } + key := getVGroupNamedKey(svc, port) + assert.Equal(t, "http", key.VGroupPort) + }) +} + +func TestHttpUpdate(t *testing.T) { + mgr := NewListenerManager(getMockCloudProvider()) + httpListener := &http{mgr: mgr} + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: klogr.New(), + } + + t.Run("remote listener is stopped", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + Status: model.Running, + } + remote := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + Status: model.Stopped, + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpListener.Update(reqCtx, action) + assert.NoError(t, err) + }) + + t.Run("forward port changed need recreate", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + ForwardPort: 443, + ListenerForward: model.OffFlag, + } + remote := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + ForwardPort: 8080, + ListenerForward: model.OnFlag, + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpListener.Update(reqCtx, action) + assert.NoError(t, err) + }) + + t.Run("forward port disabled need recreate", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + ForwardPort: 0, + ListenerForward: model.OffFlag, + } + remote := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + ForwardPort: 443, + ListenerForward: model.OnFlag, + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpListener.Update(reqCtx, action) + assert.NoError(t, err) + }) + + t.Run("listener forward is on skip update", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + ForwardPort: 0, + } + remote := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + ListenerForward: model.OnFlag, + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpListener.Update(reqCtx, action) + assert.NoError(t, err) + }) + + t.Run("no change skip update", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + Scheduler: "rr", + } + remote := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + Scheduler: "rr", + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpListener.Update(reqCtx, action) + assert.NoError(t, err) + }) + + t.Run("normal update", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + Scheduler: "wrr", + } + remote := model.ListenerAttribute{ + ListenerPort: 80, + Protocol: model.HTTP, + Scheduler: "rr", + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpListener.Update(reqCtx, action) + assert.NoError(t, err) + }) +} + +func TestHttpsUpdate(t *testing.T) { + mgr := NewListenerManager(getMockCloudProvider()) + httpsListener := &https{mgr: mgr} + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: klogr.New(), + } + + t.Run("remote listener is stopped", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 443, + Protocol: model.HTTPS, + Status: model.Running, + } + remote := model.ListenerAttribute{ + ListenerPort: 443, + Protocol: model.HTTPS, + Status: model.Stopped, + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpsListener.Update(reqCtx, action) + assert.NoError(t, err) + }) + + t.Run("cert id changed", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 443, + Protocol: model.HTTPS, + CertId: "cert-new-id", + } + remote := model.ListenerAttribute{ + ListenerPort: 443, + Protocol: model.HTTPS, + CertId: "cert-old-id", + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpsListener.Update(reqCtx, action) + assert.NoError(t, err) + }) + + t.Run("no change skip update", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 443, + Protocol: model.HTTPS, + Scheduler: "rr", + } + remote := model.ListenerAttribute{ + ListenerPort: 443, + Protocol: model.HTTPS, + Scheduler: "rr", + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpsListener.Update(reqCtx, action) + assert.NoError(t, err) + }) + + t.Run("normal update", func(t *testing.T) { + local := model.ListenerAttribute{ + ListenerPort: 443, + Protocol: model.HTTPS, + Scheduler: "wrr", + } + remote := model.ListenerAttribute{ + ListenerPort: 443, + Protocol: model.HTTPS, + Scheduler: "rr", + } + action := UpdateAction{ + lbId: "lb-test-id", + local: local, + remote: remote, + } + err := httpsListener.Update(reqCtx, action) + assert.NoError(t, err) + }) +} + +func TestCheckCertValidity(t *testing.T) { + cloud := getMockCloudProvider() + + t.Run("old cert is nil", func(t *testing.T) { + err := checkCertValidity(cloud, "cert-not-found", "cert-new-id") + assert.NoError(t, err) + }) + + t.Run("new cert is nil", func(t *testing.T) { + err := checkCertValidity(cloud, "cert-old-id", "cert-not-found") + assert.Error(t, err) + assert.Contains(t, err.Error(), "can not found cert by id") + }) + + t.Run("both certs are nil", func(t *testing.T) { + err := checkCertValidity(cloud, "cert-not-found", "cert-not-found-1") + assert.NoError(t, err) + }) + + t.Run("old cert is expired", func(t *testing.T) { + err := checkCertValidity(cloud, "cert-expired", "cert-test-1") + assert.NoError(t, err) + }) + + t.Run("new cert is expired", func(t *testing.T) { + err := checkCertValidity(cloud, "cert-test-1", "cert-expired") + assert.Error(t, err) + }) +} + +func TestParseDomainExtensionsAnnotation(t *testing.T) { + t.Run("empty annotation", func(t *testing.T) { + result, err := parseDomainExtensionsAnnotation("") + assert.NoError(t, err) + assert.Equal(t, []model.DomainExtension{}, result) + }) + + t.Run("single domain extension", func(t *testing.T) { + result, err := parseDomainExtensionsAnnotation("domain1:certId1") + assert.NoError(t, err) + expected := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "certId1"}, + } + assert.Equal(t, expected, result) + }) + + t.Run("multiple domain extensions", func(t *testing.T) { + result, err := parseDomainExtensionsAnnotation("domain1:certId1,domain2:certId2") + assert.NoError(t, err) + expected := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "certId1"}, + {Domain: "domain2", ServerCertificateId: "certId2"}, + } + assert.Equal(t, expected, result) + }) + + t.Run("domain extensions with spaces", func(t *testing.T) { + result, err := parseDomainExtensionsAnnotation("domain1:certId1, domain2:certId2 ") + assert.NoError(t, err) + expected := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "certId1"}, + {Domain: "domain2", ServerCertificateId: "certId2"}, + } + assert.Equal(t, expected, result) + }) + + t.Run("invalid format - missing separator", func(t *testing.T) { + result, err := parseDomainExtensionsAnnotation("domain1certId1") + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "invalid domain extension") + }) + + t.Run("invalid format - extra separator", func(t *testing.T) { + result, err := parseDomainExtensionsAnnotation("domain1:certId1:extra") + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "invalid domain extension") + }) +} + +func TestDiffDomainExtensions(t *testing.T) { + t.Run("no changes", func(t *testing.T) { + local := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1"}, + } + remote := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1"}, + } + toAdd, toDelete, toUpdate := diffDomainExtensions(local, remote) + assert.Empty(t, toAdd) + assert.Empty(t, toDelete) + assert.Empty(t, toUpdate) + }) + + t.Run("add domain extension", func(t *testing.T) { + local := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1"}, + {Domain: "domain2", ServerCertificateId: "cert2"}, + } + remote := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1"}, + } + toAdd, toDelete, toUpdate := diffDomainExtensions(local, remote) + assert.Equal(t, []model.DomainExtension{{Domain: "domain2", ServerCertificateId: "cert2"}}, toAdd) + assert.Empty(t, toDelete) + assert.Empty(t, toUpdate) + }) + + t.Run("delete domain extension", func(t *testing.T) { + local := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1"}, + } + remote := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1"}, + {Domain: "domain2", ServerCertificateId: "cert2"}, + } + toAdd, toDelete, toUpdate := diffDomainExtensions(local, remote) + assert.Empty(t, toAdd) + assert.Equal(t, []model.DomainExtension{{Domain: "domain2", ServerCertificateId: "cert2"}}, toDelete) + assert.Empty(t, toUpdate) + }) + + t.Run("update domain extension", func(t *testing.T) { + local := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1_new"}, + } + remote := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1_old"}, + } + toAdd, toDelete, toUpdate := diffDomainExtensions(local, remote) + // Note: In the original function, the update logic modifies the local object + // So the ServerCertificateId becomes the remote value + expectedUpdate := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1_old"}, + } + assert.Empty(t, toAdd) + assert.Empty(t, toDelete) + assert.Equal(t, expectedUpdate, toUpdate) + }) + + t.Run("complex scenario", func(t *testing.T) { + local := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1_new"}, // update + {Domain: "domain3", ServerCertificateId: "cert3"}, // add + } + remote := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1_old"}, // update + {Domain: "domain2", ServerCertificateId: "cert2"}, // delete + } + toAdd, toDelete, toUpdate := diffDomainExtensions(local, remote) + assert.Equal(t, []model.DomainExtension{{Domain: "domain3", ServerCertificateId: "cert3"}}, toAdd) + assert.Equal(t, []model.DomainExtension{{Domain: "domain2", ServerCertificateId: "cert2"}}, toDelete) + expectedUpdate := []model.DomainExtension{ + {Domain: "domain1", ServerCertificateId: "cert1_old"}, + } + assert.Equal(t, expectedUpdate, toUpdate) + }) +} diff --git a/pkg/controller/service/clbv1/loadbalancer_test.go b/pkg/controller/service/clbv1/loadbalancer_test.go index b16f1e6f1..de0e3b2ef 100644 --- a/pkg/controller/service/clbv1/loadbalancer_test.go +++ b/pkg/controller/service/clbv1/loadbalancer_test.go @@ -7,8 +7,11 @@ import ( "github.com/aliyun/alibaba-cloud-sdk-go/services/vpc" "github.com/stretchr/testify/assert" + ctrlcfg "k8s.io/cloud-provider-alibaba-cloud/pkg/config" + "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" "k8s.io/cloud-provider-alibaba-cloud/pkg/model" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model/tag" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" "k8s.io/cloud-provider-alibaba-cloud/pkg/util" ) @@ -100,3 +103,465 @@ func TestSetAvailableVswitchID(t *testing.T) { assert.Error(t, err2) assert.Contains(t, err2.Error(), "no available vsw found") } + +func TestEqualsAddressIPVersion(t *testing.T) { + cases := []struct { + name string + local model.AddressIPVersionType + remote model.AddressIPVersionType + expected bool + }{ + { + name: "both empty", + local: "", + remote: "", + expected: true, + }, + { + name: "local empty, remote ipv4", + local: "", + remote: model.IPv4, + expected: true, + }, + { + name: "local ipv4, remote empty", + local: model.IPv4, + remote: "", + expected: true, + }, + { + name: "both ipv4", + local: model.IPv4, + remote: model.IPv4, + expected: true, + }, + { + name: "both ipv6", + local: model.IPv6, + remote: model.IPv6, + expected: true, + }, + { + name: "local ipv4, remote ipv6", + local: model.IPv4, + remote: model.IPv6, + expected: false, + }, + { + name: "local empty, remote ipv6", + local: "", + remote: model.IPv6, + expected: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := equalsAddressIPVersion(c.local, c.remote) + assert.Equal(t, c.expected, result) + }) + } +} + +func TestSetModelDefaultValue(t *testing.T) { + mgr := NewLoadBalancerManager(getMockCloudProvider()) + + t.Run("set default address type", func(t *testing.T) { + mdl := &model.LoadBalancer{} + svc := getDefaultService() + anno := annotation.NewAnnotationRequest(svc) + err := setModelDefaultValue(mgr, mdl, anno) + assert.NoError(t, err) + assert.Equal(t, model.InternetAddressType, mdl.LoadBalancerAttribute.AddressType) + }) + + t.Run("set default loadbalancer name", func(t *testing.T) { + mdl := &model.LoadBalancer{} + svc := getDefaultService() + anno := annotation.NewAnnotationRequest(svc) + err := setModelDefaultValue(mgr, mdl, anno) + assert.NoError(t, err) + assert.NotEmpty(t, mdl.LoadBalancerAttribute.LoadBalancerName) + }) + + t.Run("intranet with vswitch", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[annotation.Annotation(annotation.AddressType)] = string(model.IntranetAddressType) + // Provide VSwitchId to avoid calling metadata VswitchID() which is unimplemented in mock + svc.Annotations[annotation.Annotation(annotation.VswitchId)] = "vsw-test" + reqCtx := getReqCtx(svc) + mdl := &model.LoadBalancer{ + NamespacedName: util.NamespacedName(svc), + } + // Build local model first to populate fields from annotations + err := mgr.BuildLocalModel(reqCtx, mdl) + assert.NoError(t, err) + // Then set default values + err = setModelDefaultValue(mgr, mdl, reqCtx.Anno) + assert.NoError(t, err) + assert.NotEmpty(t, mdl.LoadBalancerAttribute.VpcId) + assert.Equal(t, "vsw-test", mdl.LoadBalancerAttribute.VSwitchId) + }) + + t.Run("set default spec for paybybandwidth", func(t *testing.T) { + mdl := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + InstanceChargeType: model.PayBySpec, + }, + } + svc := getDefaultService() + anno := annotation.NewAnnotationRequest(svc) + err := setModelDefaultValue(mgr, mdl, anno) + assert.NoError(t, err) + assert.NotEmpty(t, mdl.LoadBalancerAttribute.LoadBalancerSpec) + }) + + t.Run("set paybyclcu", func(t *testing.T) { + mdl := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + InstanceChargeType: model.PayByCLCU, + }, + } + svc := getDefaultService() + anno := annotation.NewAnnotationRequest(svc) + err := setModelDefaultValue(mgr, mdl, anno) + assert.NoError(t, err) + assert.Equal(t, model.PayByCLCU, mdl.LoadBalancerAttribute.InstanceChargeType) + }) + + t.Run("intranet classic network sets vpc from config", func(t *testing.T) { + oldVpcID := ctrlcfg.CloudCFG.Global.VpcID + defer func() { ctrlcfg.CloudCFG.Global.VpcID = oldVpcID }() + ctrlcfg.CloudCFG.Global.VpcID = "vpc-classic-test" + mdl := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + AddressType: model.IntranetAddressType, + NetworkType: model.ClassicNetworkType, + VpcId: "", + VSwitchId: "vsw-1", + }, + } + svc := getDefaultService() + anno := annotation.NewAnnotationRequest(svc) + err := setModelDefaultValue(mgr, mdl, anno) + assert.NoError(t, err) + assert.Equal(t, "vpc-classic-test", mdl.LoadBalancerAttribute.VpcId) + }) + + t.Run("set default delete protection and modification protection", func(t *testing.T) { + mdl := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + DeleteProtection: "", + ModificationProtectionStatus: "", + }, + } + svc := getDefaultService() + anno := annotation.NewAnnotationRequest(svc) + err := setModelDefaultValue(mgr, mdl, anno) + assert.NoError(t, err) + assert.Equal(t, model.FlagType(model.OnFlag), mdl.LoadBalancerAttribute.DeleteProtection) + assert.Equal(t, model.ModificationProtectionType(model.ConsoleProtection), mdl.LoadBalancerAttribute.ModificationProtectionStatus) + assert.NotEmpty(t, mdl.LoadBalancerAttribute.ModificationProtectionReason) + }) + + t.Run("set default resource group id from config", func(t *testing.T) { + oldRG := ctrlcfg.CloudCFG.Global.ResourceGroupID + defer func() { ctrlcfg.CloudCFG.Global.ResourceGroupID = oldRG }() + ctrlcfg.CloudCFG.Global.ResourceGroupID = "rg-default-test" + mdl := &model.LoadBalancer{} + svc := getDefaultService() + anno := annotation.NewAnnotationRequest(svc) + err := setModelDefaultValue(mgr, mdl, anno) + assert.NoError(t, err) + assert.Equal(t, "rg-default-test", mdl.LoadBalancerAttribute.ResourceGroupId) + }) + + t.Run("append default tags", func(t *testing.T) { + mdl := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + Tags: []tag.Tag{{Key: "existing", Value: "tag"}}, + }, + } + svc := getDefaultService() + anno := annotation.NewAnnotationRequest(svc) + err := setModelDefaultValue(mgr, mdl, anno) + assert.NoError(t, err) + assert.True(t, len(mdl.LoadBalancerAttribute.Tags) >= 1) + assert.Equal(t, "existing", mdl.LoadBalancerAttribute.Tags[len(mdl.LoadBalancerAttribute.Tags)-1].Key) + }) +} + +func TestAddTagIfNotExist(t *testing.T) { + mgr := NewLoadBalancerManager(getMockCloudProvider()) + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + t.Run("tag not exist, should add", func(t *testing.T) { + remote := model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + Tags: []tag.Tag{ + {Key: "key1", Value: "value1"}, + }, + }, + } + newTag := tag.Tag{Key: "key2", Value: "value2"} + err := mgr.addTagIfNotExist(reqCtx, remote, newTag) + assert.NoError(t, err) + }) + + t.Run("tag already exist, should skip", func(t *testing.T) { + remote := model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + Tags: []tag.Tag{ + {Key: "key1", Value: "value1"}, + }, + }, + } + newTag := tag.Tag{Key: "key1", Value: "value2"} + err := mgr.addTagIfNotExist(reqCtx, remote, newTag) + assert.NoError(t, err) + }) + + t.Run("too many tags", func(t *testing.T) { + var tags []tag.Tag + for i := 0; i < MaxLBTagNum; i++ { + tags = append(tags, tag.Tag{Key: "key", Value: "value"}) + } + remote := model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + Tags: tags, + }, + } + newTag := tag.Tag{Key: "newkey", Value: "newvalue"} + err := mgr.addTagIfNotExist(reqCtx, remote, newTag) + assert.NoError(t, err) + }) +} + +func TestUpdateInstanceChargeTypeAndInstanceSpec(t *testing.T) { + mgr := NewLoadBalancerManager(getMockCloudProvider()) + + t.Run("no change", func(t *testing.T) { + svc := getDefaultService() + reqCtx := getReqCtx(svc) + local := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + InstanceChargeType: model.PayBySpec, + LoadBalancerSpec: model.S1Small, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + InstanceChargeType: model.PayBySpec, + LoadBalancerSpec: model.S1Small, + }, + } + err := mgr.updateInstanceChargeTypeAndInstanceSpec(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("change charge type", func(t *testing.T) { + svc := getDefaultService() + reqCtx := getReqCtx(svc) + local := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + InstanceChargeType: model.PayByCLCU, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + InstanceChargeType: model.PayBySpec, + LoadBalancerSpec: model.S1Small, + }, + } + err := mgr.updateInstanceChargeTypeAndInstanceSpec(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("change spec", func(t *testing.T) { + svc := getDefaultService() + reqCtx := getReqCtx(svc) + local := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + InstanceChargeType: model.PayBySpec, + LoadBalancerSpec: model.S1Small, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + InstanceChargeType: model.PayBySpec, + LoadBalancerSpec: model.S1Small, + }, + } + err := mgr.updateInstanceChargeTypeAndInstanceSpec(reqCtx, local, remote) + assert.NoError(t, err) + }) +} + +func TestLoadBalancerManager_BuildLocalModel(t *testing.T) { + mgr := NewLoadBalancerManager(getMockCloudProvider()) + + t.Run("build basic model", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[annotation.Annotation(annotation.AddressType)] = string(model.InternetAddressType) + svc.Annotations[annotation.Annotation(annotation.Bandwidth)] = "5" + reqCtx := getReqCtx(svc) + mdl := &model.LoadBalancer{ + NamespacedName: util.NamespacedName(svc), + } + + err := mgr.BuildLocalModel(reqCtx, mdl) + assert.NoError(t, err) + assert.Equal(t, model.InternetAddressType, mdl.LoadBalancerAttribute.AddressType) + assert.Equal(t, 5, mdl.LoadBalancerAttribute.Bandwidth) + }) + + t.Run("build with user managed lb", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[annotation.Annotation(annotation.LoadBalancerId)] = "lb-test" + reqCtx := getReqCtx(svc) + mdl := &model.LoadBalancer{ + NamespacedName: util.NamespacedName(svc), + } + + err := mgr.BuildLocalModel(reqCtx, mdl) + assert.NoError(t, err) + assert.True(t, mdl.LoadBalancerAttribute.IsUserManaged) + assert.Equal(t, "lb-test", mdl.LoadBalancerAttribute.LoadBalancerId) + }) + + t.Run("invalid bandwidth", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[annotation.Annotation(annotation.Bandwidth)] = "invalid" + reqCtx := getReqCtx(svc) + mdl := &model.LoadBalancer{ + NamespacedName: util.NamespacedName(svc), + } + + err := mgr.BuildLocalModel(reqCtx, mdl) + assert.Error(t, err) + assert.Contains(t, err.Error(), "bandwidth must be integer") + }) +} + +func TestLoadBalancerManager_UpdateLoadBalancerTags(t *testing.T) { + mgr := NewLoadBalancerManager(getMockCloudProvider()) + + t.Run("add new tags", func(t *testing.T) { + svc := getDefaultService() + reqCtx := getReqCtx(svc) + local := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + Tags: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + Tags: []tag.Tag{ + {Key: "key1", Value: "value1"}, + }, + }, + } + + err := mgr.updateLoadBalancerTags(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("remove tags", func(t *testing.T) { + svc := getDefaultService() + reqCtx := getReqCtx(svc) + local := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + Tags: []tag.Tag{ + {Key: "key1", Value: "value1"}, + }, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + Tags: []tag.Tag{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + }, + }, + } + + err := mgr.updateLoadBalancerTags(reqCtx, local, remote) + assert.NoError(t, err) + }) +} + +func TestLoadBalancerManager_CleanupLoadBalancerTags(t *testing.T) { + mgr := NewLoadBalancerManager(getMockCloudProvider()) + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + t.Run("cleanup tags", func(t *testing.T) { + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + Tags: []tag.Tag{ + {Key: "kubernetes.io/cluster/test", Value: "owned"}, + {Key: "otherkey", Value: "value"}, + }, + }, + } + + err := mgr.CleanupLoadBalancerTags(reqCtx, remote) + assert.NoError(t, err) + }) + + t.Run("empty lb id", func(t *testing.T) { + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "", + }, + } + + err := mgr.CleanupLoadBalancerTags(reqCtx, remote) + assert.NoError(t, err) + }) +} + +func TestLoadBalancerManager_SetProtectionsOff(t *testing.T) { + mgr := NewLoadBalancerManager(getMockCloudProvider()) + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + t.Run("turn off protections", func(t *testing.T) { + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + DeleteProtection: model.OnFlag, + ModificationProtectionStatus: model.ConsoleProtection, + }, + } + + err := mgr.SetProtectionsOff(reqCtx, remote) + assert.NoError(t, err) + }) + + t.Run("protections already off", func(t *testing.T) { + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{ + LoadBalancerId: "lb-test", + DeleteProtection: model.OffFlag, + ModificationProtectionStatus: model.NonProtection, + }, + } + + err := mgr.SetProtectionsOff(reqCtx, remote) + assert.NoError(t, err) + }) +} diff --git a/pkg/controller/service/clbv1/model_applier_test.go b/pkg/controller/service/clbv1/model_applier_test.go index 636d7b16d..d045a8e8f 100644 --- a/pkg/controller/service/clbv1/model_applier_test.go +++ b/pkg/controller/service/clbv1/model_applier_test.go @@ -12,8 +12,10 @@ import ( "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" "k8s.io/cloud-provider-alibaba-cloud/pkg/model" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model/tag" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/alibaba/base" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" + "k8s.io/cloud-provider-alibaba-cloud/pkg/util" "reflect" "testing" "time" @@ -657,3 +659,305 @@ func TestBuildActionsToUpdate(t *testing.T) { }) } } + +func TestIsLoadBalancerReusable(t *testing.T) { + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + t.Run("reusable - no special tags", func(t *testing.T) { + tags := []tag.Tag{ + {Key: "env", Value: "test"}, + } + ok, reason := isLoadBalancerReusable(reqCtx, tags, "192.168.0.1") + assert.True(t, ok) + assert.Empty(t, reason) + }) + + t.Run("not reusable - has kubernetes tag", func(t *testing.T) { + tags := []tag.Tag{ + {Key: helper.TAGKEY, Value: "test"}, + } + ok, reason := isLoadBalancerReusable(reqCtx, tags, "192.168.0.1") + assert.False(t, ok) + assert.Contains(t, reason, "can not reuse loadbalancer created by kubernetes") + }) + + t.Run("not reusable - has ack.aliyun.com tag", func(t *testing.T) { + tags := []tag.Tag{ + {Key: util.ClusterTagKey, Value: "test"}, + } + ok, reason := isLoadBalancerReusable(reqCtx, tags, "192.168.0.1") + assert.False(t, ok) + assert.Contains(t, reason, "can not reuse loadbalancer created by kubernetes") + }) + + t.Run("reusable - with eip", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[annotation.Annotation(annotation.ExternalIPType)] = "eip" + reqCtx := getReqCtx(svc) + tags := []tag.Tag{} + ok, reason := isLoadBalancerReusable(reqCtx, tags, "192.168.0.1") + assert.True(t, ok) + assert.Empty(t, reason) + }) + + t.Run("not reusable - ip mismatch", func(t *testing.T) { + svc := getDefaultService() + svc.Status.LoadBalancer.Ingress = []v1.LoadBalancerIngress{ + {IP: "10.0.0.1"}, + } + reqCtx := getReqCtx(svc) + tags := []tag.Tag{} + ok, reason := isLoadBalancerReusable(reqCtx, tags, "192.168.0.1") + assert.False(t, ok) + assert.Contains(t, reason, "cannot be bound to ip") + }) + + t.Run("reusable - ip match", func(t *testing.T) { + svc := getDefaultService() + svc.Status.LoadBalancer.Ingress = []v1.LoadBalancerIngress{ + {IP: "192.168.0.1"}, + } + reqCtx := getReqCtx(svc) + tags := []tag.Tag{} + ok, reason := isLoadBalancerReusable(reqCtx, tags, "192.168.0.1") + assert.True(t, ok) + assert.Empty(t, reason) + }) + + t.Run("reusable - hostname", func(t *testing.T) { + svc := getDefaultService() + svc.Status.LoadBalancer.Ingress = []v1.LoadBalancerIngress{ + {Hostname: "test.example.com"}, + } + reqCtx := getReqCtx(svc) + tags := []tag.Tag{} + ok, reason := isLoadBalancerReusable(reqCtx, tags, "192.168.0.1") + assert.True(t, ok) + assert.Empty(t, reason) + }) +} + +func TestHasDeleteActionToPort(t *testing.T) { + t.Run("has delete action", func(t *testing.T) { + actions := []DeleteAction{ + {listener: model.ListenerAttribute{ListenerPort: 80}}, + {listener: model.ListenerAttribute{ListenerPort: 443}}, + } + result := hasDeleteActionToPort(80, actions) + assert.True(t, result) + }) + + t.Run("no delete action", func(t *testing.T) { + actions := []DeleteAction{ + {listener: model.ListenerAttribute{ListenerPort: 443}}, + } + result := hasDeleteActionToPort(80, actions) + assert.False(t, result) + }) + + t.Run("empty actions", func(t *testing.T) { + actions := []DeleteAction{} + result := hasDeleteActionToPort(80, actions) + assert.False(t, result) + }) +} + +func TestHasDeleteActionForwardedTo(t *testing.T) { + t.Run("has forward delete action", func(t *testing.T) { + action := DeleteAction{ + listener: model.ListenerAttribute{ListenerPort: 80}, + } + actions := []DeleteAction{ + { + listener: model.ListenerAttribute{ + ListenerPort: 8080, + ListenerForward: model.OnFlag, + ForwardPort: 80, + }, + }, + } + result := hasDeleteActionForwardedTo(action, actions) + assert.True(t, result) + }) + + t.Run("no forward delete action", func(t *testing.T) { + action := DeleteAction{ + listener: model.ListenerAttribute{ListenerPort: 80}, + } + actions := []DeleteAction{ + { + listener: model.ListenerAttribute{ + ListenerPort: 8080, + ListenerForward: model.OnFlag, + ForwardPort: 443, + }, + }, + } + result := hasDeleteActionForwardedTo(action, actions) + assert.False(t, result) + }) + + t.Run("forward flag off", func(t *testing.T) { + action := DeleteAction{ + listener: model.ListenerAttribute{ListenerPort: 80}, + } + actions := []DeleteAction{ + { + listener: model.ListenerAttribute{ + ListenerPort: 8080, + ListenerForward: model.OffFlag, + ForwardPort: 80, + }, + }, + } + result := hasDeleteActionForwardedTo(action, actions) + assert.False(t, result) + }) +} + +func TestBuildVGroupCreateAndUpdateActions(t *testing.T) { + reqCtx := getReqCtx(getDefaultService()) + + t.Run("create new vgroup", func(t *testing.T) { + local := &model.LoadBalancer{ + VServerGroups: []model.VServerGroup{ + {VGroupName: "vgroup-1"}, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{LoadBalancerId: "lb-1"}, + VServerGroups: []model.VServerGroup{}, + } + actions, err := buildVGroupCreateAndUpdateActions(reqCtx, local, remote) + assert.NoError(t, err) + assert.Equal(t, 1, len(actions)) + assert.Equal(t, vGroupActionCreateAndAddBackendServers, actions[0].Action) + }) + + t.Run("update existing vgroup", func(t *testing.T) { + local := &model.LoadBalancer{ + VServerGroups: []model.VServerGroup{ + { + VGroupId: "vsg-1", + VGroupName: "vgroup-1", + Backends: []model.BackendAttribute{{ServerId: "ecs-1", Port: 80}}, + }, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{LoadBalancerId: "lb-1"}, + VServerGroups: []model.VServerGroup{ + { + VGroupId: "vsg-1", + VGroupName: "vgroup-1", + Backends: []model.BackendAttribute{{ServerId: "ecs-2", Port: 80}}, + }, + }, + } + actions, err := buildVGroupCreateAndUpdateActions(reqCtx, local, remote) + assert.NoError(t, err) + assert.Equal(t, 1, len(actions)) + assert.Equal(t, vGroupActionUpdate, actions[0].Action) + }) + + t.Run("no change", func(t *testing.T) { + local := &model.LoadBalancer{ + VServerGroups: []model.VServerGroup{ + { + VGroupId: "vsg-1", + VGroupName: "vgroup-1", + Backends: []model.BackendAttribute{{ServerId: "ecs-1", Port: 80}}, + }, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{LoadBalancerId: "lb-1"}, + VServerGroups: []model.VServerGroup{ + { + VGroupId: "vsg-1", + VGroupName: "vgroup-1", + Backends: []model.BackendAttribute{{ServerId: "ecs-1", Port: 80}}, + }, + }, + } + actions, err := buildVGroupCreateAndUpdateActions(reqCtx, local, remote) + assert.NoError(t, err) + assert.Equal(t, 0, len(actions)) + }) + + t.Run("reuse vgroup by id", func(t *testing.T) { + local := &model.LoadBalancer{ + VServerGroups: []model.VServerGroup{ + { + VGroupId: "vsg-1", + VGroupName: "vgroup-new", + Backends: []model.BackendAttribute{{ServerId: "ecs-1", Port: 80}}, + }, + }, + } + remote := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{LoadBalancerId: "lb-1"}, + VServerGroups: []model.VServerGroup{ + { + VGroupId: "vsg-1", + VGroupName: "vgroup-old", + Backends: []model.BackendAttribute{{ServerId: "ecs-2", Port: 80}}, + }, + }, + } + actions, err := buildVGroupCreateAndUpdateActions(reqCtx, local, remote) + assert.NoError(t, err) + assert.Equal(t, 1, len(actions)) + assert.Equal(t, vGroupActionUpdate, actions[0].Action) + }) +} + +// TestModelApplier_Apply_ErrorCases tests error cases for Apply function +func TestModelApplier_Apply_ErrorCases(t *testing.T) { + vgm, err := getTestVGroupManager() + if err != nil { + t.Error(err) + } + applier := NewModelApplier(NewLoadBalancerManager(getMockCloudProvider()), + NewListenerManager(getMockCloudProvider()), vgm) + + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + t.Run("nil local model", func(t *testing.T) { + _, err := applier.Apply(reqCtx, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "local model is nil") + }) + + t.Run("namespaced name mismatch", func(t *testing.T) { + localModel := &model.LoadBalancer{ + NamespacedName: types.NamespacedName{ + Name: "other-service", + Namespace: "default", + }, + } + _, err := applier.Apply(reqCtx, localModel) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not match") + }) + + t.Run("preserve on delete", func(t *testing.T) { + localModel := &model.LoadBalancer{ + NamespacedName: types.NamespacedName{ + Name: SvcName, + Namespace: NS, + }, + LoadBalancerAttribute: model.LoadBalancerAttribute{ + PreserveOnDelete: true, + }, + } + svc.Annotations[annotation.LoadBalancerId] = vmock.ExistLBID + reqCtx := getReqCtx(svc) + remote, err := applier.Apply(reqCtx, localModel) + // Should not error, but should record an event + assert.NoError(t, err) + assert.NotNil(t, remote) + }) +} diff --git a/pkg/controller/service/clbv1/service_controller_test.go b/pkg/controller/service/clbv1/service_controller_test.go index 724e7ce67..78f22bc99 100644 --- a/pkg/controller/service/clbv1/service_controller_test.go +++ b/pkg/controller/service/clbv1/service_controller_test.go @@ -2,6 +2,10 @@ package clbv1 import ( "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -13,14 +17,13 @@ import ( "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model" prvd "k8s.io/cloud-provider-alibaba-cloud/pkg/provider" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" "k8s.io/cloud-provider-alibaba-cloud/pkg/util" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "testing" - "time" ) var ( @@ -365,3 +368,313 @@ func TestDryRun(t *testing.T) { t.Error(err) } } + +func TestUpdateReadinessCondition(t *testing.T) { + t.Run("successful update", func(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-1", + Namespace: NS, + }, + } + err := recon.kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + vgroups := []model.VServerGroup{ + { + InitialBackends: []model.BackendAttribute{ + { + TargetRef: &v1.ObjectReference{ + Namespace: NS, + Name: "test-pod-1", + }, + }, + }, + }, + } + + err = recon.updateReadinessCondition(reqCtx, vgroups) + assert.NoError(t, err) + }) + + t.Run("TargetRef is nil", func(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + vgroups := []model.VServerGroup{ + { + InitialBackends: []model.BackendAttribute{ + { + TargetRef: nil, + }, + }, + }, + } + + err := recon.updateReadinessCondition(reqCtx, vgroups) + assert.NoError(t, err) + }) + + t.Run("duplicate pod", func(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-2", + Namespace: NS, + }, + } + err := recon.kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + vgroups := []model.VServerGroup{ + { + InitialBackends: []model.BackendAttribute{ + { + TargetRef: &v1.ObjectReference{ + Namespace: NS, + Name: "test-pod-2", + }, + }, + { + TargetRef: &v1.ObjectReference{ + Namespace: NS, + Name: "test-pod-2", + }, + }, + }, + }, + } + + err = recon.updateReadinessCondition(reqCtx, vgroups) + assert.NoError(t, err) + }) + + t.Run("pod not found", func(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + vgroups := []model.VServerGroup{ + { + InitialBackends: []model.BackendAttribute{ + { + TargetRef: &v1.ObjectReference{ + Namespace: NS, + Name: "non-existent-pod", + }, + }, + }, + }, + } + + err := recon.updateReadinessCondition(reqCtx, vgroups) + assert.NoError(t, err) + }) + + t.Run("multiple vgroups", func(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + pod1 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-3", + Namespace: NS, + }, + } + pod2 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-4", + Namespace: NS, + }, + } + err := recon.kubeClient.Create(context.TODO(), pod1) + assert.NoError(t, err) + err = recon.kubeClient.Create(context.TODO(), pod2) + assert.NoError(t, err) + + vgroups := []model.VServerGroup{ + { + InitialBackends: []model.BackendAttribute{ + { + TargetRef: &v1.ObjectReference{ + Namespace: NS, + Name: "test-pod-3", + }, + }, + }, + }, + { + InitialBackends: []model.BackendAttribute{ + { + TargetRef: &v1.ObjectReference{ + Namespace: NS, + Name: "test-pod-4", + }, + }, + }, + }, + } + + err = recon.updateReadinessCondition(reqCtx, vgroups) + assert.NoError(t, err) + }) + + t.Run("empty vgroups", func(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + vgroups := []model.VServerGroup{} + + err := recon.updateReadinessCondition(reqCtx, vgroups) + assert.NoError(t, err) + }) + + t.Run("InvalidBackends ConditionFalse path", func(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid-backend-pod", Namespace: NS}, + } + err := recon.kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + vgroups := []model.VServerGroup{ + { + InvalidBackends: []model.BackendAttribute{ + { + TargetRef: &v1.ObjectReference{Namespace: NS, Name: "invalid-backend-pod"}, + }, + }, + }, + } + err = recon.updateReadinessCondition(reqCtx, vgroups) + assert.NoError(t, err) + }) + + t.Run("ENI Backends path", func(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + svc.Annotations = map[string]string{helper.BackendType: model.ENIBackendType} + reqCtx := getReqCtx(svc) + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "eni-backend-pod", Namespace: NS}, + } + err := recon.kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + vgroups := []model.VServerGroup{ + { + Backends: []model.BackendAttribute{ + {TargetRef: &v1.ObjectReference{Namespace: NS, Name: "eni-backend-pod"}}, + }, + }, + } + err = recon.updateReadinessCondition(reqCtx, vgroups) + assert.NoError(t, err) + }) +} + +func TestRemoveServiceStatus(t *testing.T) { + t.Run("no change when status already empty", func(t *testing.T) { + recon := getReconcileService() + svc := &v1.Service{} + err := recon.kubeClient.Get(context.TODO(), types.NamespacedName{Namespace: NS, Name: SvcName}, svc) + assert.NoError(t, err) + reqCtx := getReqCtx(svc) + err = recon.removeServiceStatus(reqCtx, svc) + assert.NoError(t, err) + }) + + t.Run("patch to clear status", func(t *testing.T) { + recon := getReconcileService() + svc := &v1.Service{} + err := recon.kubeClient.Get(context.TODO(), types.NamespacedName{Namespace: NS, Name: SvcName}, svc) + assert.NoError(t, err) + svc.Status.LoadBalancer = v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{{IP: "1.2.3.4"}}, + } + err = recon.kubeClient.Status().Update(context.TODO(), svc) + assert.NoError(t, err) + + reqCtx := getReqCtx(svc) + err = recon.removeServiceStatus(reqCtx, svc) + assert.NoError(t, err) + + updated := &v1.Service{} + err = recon.kubeClient.Get(context.TODO(), types.NamespacedName{Namespace: NS, Name: SvcName}, updated) + assert.NoError(t, err) + assert.Empty(t, updated.Status.LoadBalancer.Ingress) + }) +} + +func TestRemoveServiceLabels(t *testing.T) { + t.Run("no op when no target labels", func(t *testing.T) { + recon := getReconcileService() + svc := &v1.Service{} + err := recon.kubeClient.Get(context.TODO(), types.NamespacedName{Namespace: NS, Name: SvcName}, svc) + assert.NoError(t, err) + err = recon.removeServiceLabels(svc) + assert.NoError(t, err) + }) + + t.Run("remove labels when present", func(t *testing.T) { + recon := getReconcileService() + svc := &v1.Service{} + err := recon.kubeClient.Get(context.TODO(), types.NamespacedName{Namespace: NS, Name: SvcName}, svc) + assert.NoError(t, err) + if svc.Labels == nil { + svc.Labels = make(map[string]string) + } + svc.Labels[helper.LabelServiceHash] = "hash1" + svc.Labels[helper.LabelLoadBalancerId] = "lb-1" + err = recon.kubeClient.Update(context.TODO(), svc) + assert.NoError(t, err) + + svc2 := &v1.Service{} + err = recon.kubeClient.Get(context.TODO(), types.NamespacedName{Namespace: NS, Name: SvcName}, svc2) + assert.NoError(t, err) + err = recon.removeServiceLabels(svc2) + assert.NoError(t, err) + }) +} + +func TestReconcileService_ReconcileLoadBalancerResources(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + err := recon.reconcileLoadBalancerResources(reqCtx) + assert.NoError(t, err) +} + +func TestReconcileService_UpdateServiceStatus_LBNil(t *testing.T) { + recon := getReconcileService() + svc := getDefaultService() + reqCtx := getReqCtx(svc) + + err := recon.updateServiceStatus(reqCtx, svc, nil) + assert.Error(t, err) +} + +func TestHasInvalidBackendInVServerGroups(t *testing.T) { + assert.False(t, hasInvalidBackendInVServerGroups(nil)) + assert.False(t, hasInvalidBackendInVServerGroups([]model.VServerGroup{ + {InvalidBackends: nil}, + })) + assert.True(t, hasInvalidBackendInVServerGroups([]model.VServerGroup{ + {InvalidBackends: []model.BackendAttribute{{ServerId: "i-1"}}}, + })) +} diff --git a/pkg/controller/service/clbv1/vgroups_test.go b/pkg/controller/service/clbv1/vgroups_test.go index 38e935cc4..718382877 100644 --- a/pkg/controller/service/clbv1/vgroups_test.go +++ b/pkg/controller/service/clbv1/vgroups_test.go @@ -3,15 +3,23 @@ package clbv1 import ( "context" "fmt" + "net" + "os" "testing" "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/backend" svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" "k8s.io/cloud-provider-alibaba-cloud/pkg/model" + prvd "k8s.io/cloud-provider-alibaba-cloud/pkg/provider" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/dryrun" + "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" "k8s.io/klog/v2/klogr" ) @@ -68,8 +76,6 @@ func TestVGroupManager_BatchSyncVServerGroupBackendServers(t *testing.T) { assert.Equal(t, len(vgroup.Backends), 200) } -// --- setWeightBackends --- - func TestSetWeightBackends_NilWeightNilDefault_UsesDefaultServerWeight(t *testing.T) { backends := []model.BackendAttribute{ {ServerId: "ecs-1", Type: model.ECSBackendType}, @@ -182,3 +188,1165 @@ func TestBuildVGroupForServicePort_InvalidDefaultWeight_Negative(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "default weight must be integer in range [0,100]") } + +func TestGetVGroupIDs(t *testing.T) { + cases := []struct { + name string + annotation string + expected []string + expectError bool + }{ + { + name: "empty annotation", + annotation: "", + expected: nil, + expectError: false, + }, + { + name: "single vgroup", + annotation: "vsg-123:80", + expected: []string{"vsg-123"}, + expectError: false, + }, + { + name: "multiple vgroups", + annotation: "vsg-123:80,vsg-456:443", + expected: []string{"vsg-123", "vsg-456"}, + expectError: false, + }, + { + name: "invalid format", + annotation: "vsg-123", + expected: nil, + expectError: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := getVGroupIDs(c.annotation) + if c.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, c.expected, result) + } + }) + } +} + +func TestContainsIP(t *testing.T) { + _, cidr1, _ := net.ParseCIDR("192.168.0.0/24") + _, cidr2, _ := net.ParseCIDR("10.0.0.0/8") + cidrs := []*net.IPNet{cidr1, cidr2} + + cases := []struct { + name string + serverIp string + expected bool + }{ + { + name: "ip in first cidr", + serverIp: "192.168.0.1", + expected: true, + }, + { + name: "ip in second cidr", + serverIp: "10.0.0.1", + expected: true, + }, + { + name: "ip not in cidrs", + serverIp: "172.16.0.1", + expected: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := containsIP(cidrs, c.serverIp) + assert.Equal(t, c.expected, result) + }) + } +} + +func TestPodNumberAlgorithm(t *testing.T) { + t.Run("eni mode", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "eni-1", Weight: 50}, + {ServerId: "eni-2", Weight: 50}, + } + result := podNumberAlgorithm(helper.ENITrafficPolicy, backends, DefaultServerWeight) + assert.Equal(t, DefaultServerWeight, result[0].Weight) + assert.Equal(t, DefaultServerWeight, result[1].Weight) + }) + + t.Run("cluster mode", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "ecs-1", Weight: 50}, + {ServerId: "ecs-2", Weight: 50}, + } + result := podNumberAlgorithm(helper.ClusterTrafficPolicy, backends, DefaultServerWeight) + assert.Equal(t, DefaultServerWeight, result[0].Weight) + assert.Equal(t, DefaultServerWeight, result[1].Weight) + }) + + t.Run("local mode", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "ecs-1", Weight: 0, Type: model.ECSBackendType}, + {ServerId: "ecs-1", Weight: 0, Type: model.ECSBackendType}, + {ServerId: "ecs-2", Weight: 0, Type: model.ECSBackendType}, + } + result := podNumberAlgorithm(helper.LocalTrafficPolicy, backends, DefaultServerWeight) + assert.Equal(t, 2, result[0].Weight) + assert.Equal(t, 2, result[1].Weight) + assert.Equal(t, 1, result[2].Weight) + }) +} + +func TestPodPercentAlgorithm(t *testing.T) { + t.Run("empty backends", func(t *testing.T) { + backends := []model.BackendAttribute{} + result := podPercentAlgorithm(helper.ENITrafficPolicy, backends, 100) + assert.Equal(t, 0, len(result)) + }) + + t.Run("zero weight", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "eni-1"}, + {ServerId: "eni-2"}, + } + result := podPercentAlgorithm(helper.ENITrafficPolicy, backends, 0) + assert.Equal(t, 0, result[0].Weight) + assert.Equal(t, 0, result[1].Weight) + }) + + t.Run("eni mode", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "eni-1"}, + {ServerId: "eni-2"}, + } + result := podPercentAlgorithm(helper.ENITrafficPolicy, backends, 100) + assert.Equal(t, 50, result[0].Weight) + assert.Equal(t, 50, result[1].Weight) + }) + + t.Run("cluster mode", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "ecs-1"}, + {ServerId: "ecs-2"}, + {ServerId: "ecs-3"}, + {ServerId: "ecs-4"}, + } + result := podPercentAlgorithm(helper.ClusterTrafficPolicy, backends, 100) + assert.Equal(t, 25, result[0].Weight) + }) + + t.Run("local mode", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "ecs-1", Type: model.ECSBackendType}, + {ServerId: "ecs-1", Type: model.ECSBackendType}, + {ServerId: "ecs-2", Type: model.ECSBackendType}, + } + result := podPercentAlgorithm(helper.LocalTrafficPolicy, backends, 100) + assert.Equal(t, 66, result[0].Weight) + assert.Equal(t, 66, result[1].Weight) + assert.Equal(t, 33, result[2].Weight) + }) + + t.Run("minimum weight", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "eni-1"}, + {ServerId: "eni-2"}, + } + result := podPercentAlgorithm(helper.ENITrafficPolicy, backends, 1) + assert.Equal(t, 1, result[0].Weight) + assert.Equal(t, 1, result[1].Weight) + }) +} + +func TestRemoveDuplicatedECS(t *testing.T) { + t.Run("no duplicates", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "ecs-1"}, + {ServerId: "ecs-2"}, + } + result := removeDuplicatedECS(backends) + assert.Equal(t, 2, len(result)) + }) + + t.Run("with duplicates", func(t *testing.T) { + backends := []model.BackendAttribute{ + {ServerId: "ecs-1", Type: model.ECSBackendType}, + {ServerId: "ecs-1", Type: model.ECSBackendType}, + {ServerId: "ecs-2", Type: model.ECSBackendType}, + } + result := removeDuplicatedECS(backends) + assert.Equal(t, 2, len(result)) + assert.Equal(t, "ecs-1", result[0].ServerId) + assert.Equal(t, "ecs-2", result[1].ServerId) + }) + + t.Run("empty backends", func(t *testing.T) { + backends := []model.BackendAttribute{} + result := removeDuplicatedECS(backends) + assert.Equal(t, 0, len(result)) + }) +} + +func TestDiff(t *testing.T) { + t.Run("all same", func(t *testing.T) { + remote := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 100}, + }, + } + local := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 100}, + }, + } + add, del, update := diff(remote, local) + _ = add + _ = del + _ = update + assert.Equal(t, 0, len(add)) + assert.Equal(t, 0, len(del)) + assert.Equal(t, 0, len(update)) + }) + + t.Run("add backends", func(t *testing.T) { + remote := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 100}, + }, + } + local := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 100}, + {ServerId: "ecs-2", Port: 80, Weight: 100}, + }, + } + add, del, update := diff(remote, local) + assert.Equal(t, 1, len(add)) + assert.Equal(t, "ecs-2", add[0].ServerId) + assert.Equal(t, 0, len(del)) + assert.Equal(t, 0, len(update)) + }) + + t.Run("delete backends", func(t *testing.T) { + remote := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 100}, + {ServerId: "ecs-2", Port: 80, Weight: 100}, + }, + } + local := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 100}, + }, + } + add, del, update := diff(remote, local) + assert.Equal(t, 0, len(add)) + assert.Equal(t, 1, len(del)) + assert.Equal(t, "ecs-2", del[0].ServerId) + assert.Equal(t, 0, len(update)) + }) + + t.Run("update weight", func(t *testing.T) { + remote := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 100}, + }, + } + local := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 50}, + }, + } + add, del, update := diff(remote, local) + assert.Equal(t, 0, len(add)) + assert.Equal(t, 0, len(del)) + assert.Equal(t, 1, len(update)) + assert.Equal(t, 50, update[0].Weight) + }) + + t.Run("skip user managed", func(t *testing.T) { + remote := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "ecs-1", Port: 80, Weight: 100, IsUserManaged: true}, + }, + } + local := model.VServerGroup{ + Backends: []model.BackendAttribute{}, + } + add, del, update := diff(remote, local) + assert.Equal(t, 0, len(add)) + assert.Equal(t, 0, len(del)) + assert.Equal(t, 0, len(update)) + }) + + t.Run("eni backend", func(t *testing.T) { + remote := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "eni-1", ServerIp: "192.168.0.1", Port: 8080, Type: "eni"}, + }, + } + local := model.VServerGroup{ + Backends: []model.BackendAttribute{ + {ServerId: "eni-1", ServerIp: "192.168.0.1", Port: 8080, Type: "eni"}, + {ServerId: "eni-2", ServerIp: "192.168.0.2", Port: 8080, Type: "eni"}, + }, + } + add, del, update := diff(remote, local) + _ = del + _ = update + assert.Equal(t, 1, len(add)) + assert.Equal(t, "eni-2", add[0].ServerId) + }) +} + +func TestSetBackendsFromEndpointSlices(t *testing.T) { + mgr, err := getTestVGroupManager() + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: klogr.New(), + } + + t.Run("empty endpoint slices", func(t *testing.T) { + candidates := &backend.EndpointWithENI{ + EndpointSlices: []discovery.EndpointSlice{}, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpointSlices(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 0, len(backends)) + }) + + t.Run("ready endpoint with int target port", func(t *testing.T) { + ready := true + nodeName := "node-1" + portName := "http" + port := int32(8080) + protocol := v1.ProtocolTCP + candidates := &backend.EndpointWithENI{ + EndpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-slice", + Namespace: "default", + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.1"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpointSlices(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 1, len(backends)) + assert.Equal(t, "10.0.0.1", backends[0].ServerIp) + assert.Equal(t, 8080, backends[0].Port) + }) + + t.Run("ready endpoint with string target port", func(t *testing.T) { + ready := true + nodeName := "node-1" + portName := "http" + port := int32(8080) + protocol := v1.ProtocolTCP + candidates := &backend.EndpointWithENI{ + EndpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-slice", + Namespace: "default", + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.2"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromString("http"), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpointSlices(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 1, len(backends)) + assert.Equal(t, "10.0.0.2", backends[0].ServerIp) + assert.Equal(t, 8080, backends[0].Port) + }) + + t.Run("terminating endpoint ignored", func(t *testing.T) { + ready := true + terminating := true + nodeName := "node-1" + portName := "http" + port := int32(8080) + protocol := v1.ProtocolTCP + candidates := &backend.EndpointWithENI{ + EndpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-slice", + Namespace: "default", + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.3"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + Terminating: &terminating, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpointSlices(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 0, len(backends)) + }) + + t.Run("duplicate endpoint addresses", func(t *testing.T) { + ready := true + nodeName := "node-1" + portName := "http" + port := int32(8080) + protocol := v1.ProtocolTCP + candidates := &backend.EndpointWithENI{ + EndpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-slice", + Namespace: "default", + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.4"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + }, + { + Addresses: []string{"10.0.0.4"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpointSlices(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 1, len(backends)) + }) + + t.Run("endpoint without target ref", func(t *testing.T) { + ready := false + nodeName := "node-1" + portName := "http" + port := int32(8080) + protocol := v1.ProtocolTCP + candidates := &backend.EndpointWithENI{ + EndpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-slice", + Namespace: "default", + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.5"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + TargetRef: nil, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpointSlices(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 0, len(backends)) + }) +} + +// TestSetBackendsFromEndpoints tests the setBackendsFromEndpoints function +func TestSetBackendsFromEndpoints(t *testing.T) { + mgr, err := getTestVGroupManager() + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: klogr.New(), + } + + t.Run("empty endpoints", func(t *testing.T) { + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{}, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 0, len(backends)) + }) + + t.Run("ready endpoint with int target port", func(t *testing.T) { + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "10.0.0.1", + NodeName: stringPtr("node-1"), + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 1, len(backends)) + assert.Equal(t, "10.0.0.1", backends[0].ServerIp) + assert.Equal(t, 8080, backends[0].Port) + }) + + t.Run("ready endpoint with string target port", func(t *testing.T) { + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "10.0.0.2", + NodeName: stringPtr("node-1"), + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromString("http"), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 1, len(backends)) + assert.Equal(t, "10.0.0.2", backends[0].ServerIp) + assert.Equal(t, 8080, backends[0].Port) + }) + + t.Run("not ready endpoint without target ref", func(t *testing.T) { + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.3", + NodeName: stringPtr("node-1"), + TargetRef: nil, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 0, len(backends)) + }) + + t.Run("not ready endpoint with pod but no readiness gate", func(t *testing.T) { + // Create a fake kube client with a pod + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + } + kubeClient := getFakeKubeClient() + err := kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + mgr, err := NewVGroupManager(kubeClient, getMockCloudProvider()) + assert.NoError(t, err) + + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.4", + NodeName: stringPtr("node-1"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Namespace: "default", + Name: "test-pod", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 0, len(backends)) + }) + + t.Run("port name not found", func(t *testing.T) { + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "10.0.0.5", + NodeName: stringPtr("node-1"), + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "other", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromString("http"), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 0, len(backends)) + }) + + t.Run("not ready endpoint with non-pod target ref", func(t *testing.T) { + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.6", + NodeName: stringPtr("node-1"), + TargetRef: &v1.ObjectReference{ + Kind: "Node", + Namespace: "default", + Name: "test-node", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 0, len(backends)) + }) + + t.Run("not ready endpoint with pod not found", func(t *testing.T) { + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.7", + NodeName: stringPtr("node-1"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Namespace: "default", + Name: "not-found-pod", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.True(t, containsPotential) // Should be true when pod not found + assert.Equal(t, 0, len(backends)) + }) + + t.Run("not ready endpoint with pod with readiness gate but containers not ready", func(t *testing.T) { + // Create a fake kube client with a pod that has readiness gate but containers not ready + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-not-ready", + Namespace: "default", + }, + Spec: v1.PodSpec{ + ReadinessGates: []v1.PodReadinessGate{ + { + ConditionType: v1.PodConditionType(helper.BuildReadinessGatePodConditionTypeWithPrefix(helper.TargetHealthPodConditionServiceTypePrefix, "test")), + }, + }, + }, + Status: v1.PodStatus{ + Conditions: []v1.PodCondition{ + { + Type: v1.ContainersReady, + Status: v1.ConditionFalse, + }, + }, + }, + } + kubeClient := getFakeKubeClient() + err := kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + mgr, err := NewVGroupManager(kubeClient, getMockCloudProvider()) + assert.NoError(t, err) + + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.8", + NodeName: stringPtr("node-1"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Namespace: "default", + Name: "test-pod-not-ready", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.True(t, containsPotential) // Should be true when containers not ready + assert.Equal(t, 0, len(backends)) + }) + + t.Run("not ready endpoint with pod with readiness gate and containers ready", func(t *testing.T) { + // Create a fake kube client with a pod that has readiness gate and containers ready + readinessGateName := helper.BuildReadinessGatePodConditionTypeWithPrefix(helper.TargetHealthPodConditionServiceTypePrefix, "test") + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-ready", + Namespace: "default", + }, + Spec: v1.PodSpec{ + ReadinessGates: []v1.PodReadinessGate{ + { + ConditionType: v1.PodConditionType(readinessGateName), + }, + }, + }, + Status: v1.PodStatus{ + Conditions: []v1.PodCondition{ + { + Type: v1.ContainersReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + kubeClient := getFakeKubeClient() + err := kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + mgr, err := NewVGroupManager(kubeClient, getMockCloudProvider()) + assert.NoError(t, err) + + candidates := &backend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.9", + NodeName: stringPtr("node-1"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Namespace: "default", + Name: "test-pod-ready", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + } + vgroup := model.VServerGroup{ + VGroupName: "test-vgroup", + ServicePort: v1.ServicePort{ + Port: 80, + TargetPort: intstr.FromInt(8080), + Name: "http", + }, + } + backends, containsPotential, err := mgr.setBackendsFromEndpoints(reqCtx, candidates, vgroup) + assert.NoError(t, err) + assert.False(t, containsPotential) + assert.Equal(t, 1, len(backends)) + assert.Equal(t, "10.0.0.9", backends[0].ServerIp) + }) +} + +func stringPtr(s string) *string { + return &s +} + +// TestGetVgroupById tests the getVgroupById function +func TestGetVgroupById(t *testing.T) { + mgr, err := getTestVGroupManager() + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Annotations: map[string]string{ + annotation.Annotation(annotation.LoadBalancerId): "lb-exist-id", + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: klogr.New(), + } + + t.Run("find existing vgroup", func(t *testing.T) { + vgroup, err := getVgroupById(mgr, reqCtx, "rsp-tcp-80") + assert.NoError(t, err) + assert.NotNil(t, vgroup) + assert.Equal(t, "rsp-tcp-80", vgroup.VGroupId) + }) + + t.Run("find non-existing vgroup", func(t *testing.T) { + vgroup, err := getVgroupById(mgr, reqCtx, "rsp-not-exist") + assert.NoError(t, err) + assert.Nil(t, vgroup) + }) + + t.Run("find another existing vgroup", func(t *testing.T) { + vgroup, err := getVgroupById(mgr, reqCtx, "rsp-udp-53") + assert.NoError(t, err) + assert.NotNil(t, vgroup) + assert.Equal(t, "rsp-udp-53", vgroup.VGroupId) + }) +} + +func TestVGroupManager_BuildLocalModel(t *testing.T) { + mgr, err := getTestVGroupManager() + assert.NoError(t, err) + + t.Run("success", func(t *testing.T) { + svc := getDefaultService() + reqCtx := getReqCtx(svc) + m := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{}, + } + err = mgr.BuildLocalModel(reqCtx, m) + assert.NoError(t, err) + assert.NotNil(t, m.VServerGroups) + }) + + t.Run("with ENI backends", func(t *testing.T) { + svc := getDefaultService() + svc.Annotations[annotation.Annotation(annotation.BackendType)] = model.ENIBackendType + reqCtx := getReqCtx(svc) + m := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{}, + } + err = mgr.BuildLocalModel(reqCtx, m) + assert.NoError(t, err) + assert.NotNil(t, m.VServerGroups) + }) + + t.Run("updateVServerGroupENIBackendID DescribeNetworkInterfaces error", func(t *testing.T) { + oldEnv := os.Getenv("SERVICE_FORCE_BACKEND_ENI") + defer func() { _ = os.Setenv("SERVICE_FORCE_BACKEND_ENI", oldEnv) }() + _ = os.Setenv("SERVICE_FORCE_BACKEND_ENI", "true") + cloud := prvd.Provider(vmock.MockCloud{ + MockECS: vmock.NewMockECS(nil), + MockVPC: vmock.NewMockVPC(nil), + IMetaData: vmock.NewMockMetaData("vpc-describe-eni-error"), + }) + eniMgr, err := NewVGroupManager(getFakeKubeClient(), cloud) + assert.NoError(t, err) + svc := getDefaultService() + svc.Annotations[annotation.Annotation(annotation.BackendType)] = model.ENIBackendType + reqCtx := getReqCtx(svc) + m := &model.LoadBalancer{ + LoadBalancerAttribute: model.LoadBalancerAttribute{}, + } + err = eniMgr.BuildLocalModel(reqCtx, m) + assert.Error(t, err) + if err != nil { + assert.Contains(t, err.Error(), "DescribeNetworkInterfaces") + } + }) +} diff --git a/pkg/controller/service/nlbv2/coverage b/pkg/controller/service/nlbv2/coverage new file mode 100644 index 000000000..5f02b1119 --- /dev/null +++ b/pkg/controller/service/nlbv2/coverage @@ -0,0 +1 @@ +mode: set diff --git a/pkg/controller/service/nlbv2/coverage-nlbv2 b/pkg/controller/service/nlbv2/coverage-nlbv2 new file mode 100644 index 000000000..a60ac519f --- /dev/null +++ b/pkg/controller/service/nlbv2/coverage-nlbv2 @@ -0,0 +1,1204 @@ +mode: set +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:27.106,29.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:37.127,39.24 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:39.24,42.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:45.127,49.63 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:49.63,52.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:55.128,58.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:60.130,62.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:64.123,72.2 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:74.81,75.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:75.56,77.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:79.2,79.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:79.54,89.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:91.2,91.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:91.30,95.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:97.2,97.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:97.64,108.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:110.2,110.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:110.50,120.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:122.2,122.94 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:122.94,133.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:135.2,135.14 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:138.43,139.32 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:139.32,141.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:144.2,144.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:144.58,147.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:148.2,148.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:152.130,157.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:166.128,168.49 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:168.49,171.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:174.128,179.48 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:179.48,184.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:187.128,189.49 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:189.49,192.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:195.131,197.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:199.128,207.2 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:209.75,210.15 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:210.15,212.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:214.2,214.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:214.30,216.82 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:216.82,218.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:221.2,227.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:227.16,228.33 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:228.33,230.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:231.3,231.15 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:234.2,234.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:234.26,239.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:240.2,240.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:244.115,249.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:258.124,260.42 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:260.42,263.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:266.124,270.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:270.16,271.75 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:271.75,273.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:276.3,276.40 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:276.40,279.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:283.124,285.42 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:285.42,288.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:291.127,293.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:295.111,304.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:304.16,308.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:310.2,310.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:310.31,311.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:311.26,312.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:314.3,314.40 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:314.40,315.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:317.3,324.47 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:328.96,329.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:329.34,331.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:333.2,333.80 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:333.80,335.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:337.2,337.82 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:337.82,339.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:341.2,346.45 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:346.45,350.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:352.2,352.14 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:356.133,361.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:370.133,372.54 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:372.54,375.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:378.133,383.41 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:383.41,388.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:391.133,393.54 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:393.54,396.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:399.136,401.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:403.154,405.9 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:405.9,407.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:409.2,416.96 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:419.91,420.15 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:420.15,422.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:424.2,425.9 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:425.9,427.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:429.2,435.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:435.16,436.33 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:436.33,439.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:440.3,440.15 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:443.2,443.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:443.26,448.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:449.2,449.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:452.74,454.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:456.54,457.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:457.69,459.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:460.2,460.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:460.62,466.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:467.2,467.40 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:467.40,469.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:470.2,470.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:473.55,476.20 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:476.20,477.84 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:477.84,479.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:481.2,481.20 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:481.20,482.84 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:482.84,484.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:486.2,486.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:486.52,492.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:494.2,494.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:497.76,498.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:498.28,502.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:503.2,503.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:503.25,504.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:504.19,509.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:512.2,512.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:516.50,517.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:517.39,519.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:521.2,521.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:521.33,524.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:525.2,525.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:525.31,528.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:529.2,529.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:29.63,33.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:55.74,56.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:56.51,58.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:58.18,61.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:63.3,63.44 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:63.44,65.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:67.2,67.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:70.78,71.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:71.51,73.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:73.19,76.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:77.3,77.44 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:77.44,79.28 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:79.28,82.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:84.4,85.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:85.18,87.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:88.4,89.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:89.18,91.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:92.4,92.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:92.28,94.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:95.4,95.40 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:95.40,97.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:99.4,99.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:102.2,102.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:105.117,106.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:106.49,108.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:108.17,110.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:111.3,111.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:113.2,113.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:116.118,118.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:118.16,120.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:121.2,122.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:126.61,133.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:133.16,135.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:136.2,138.57 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:138.57,139.47 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:139.47,141.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:142.3,143.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:143.17,145.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:146.3,146.37 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:146.37,150.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:153.2,167.32 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:167.32,169.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:169.8,171.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:173.2,173.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:173.69,175.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:175.17,177.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:178.3,178.41 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:181.2,181.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:181.51,183.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:183.17,185.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:186.3,186.44 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:188.2,188.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:188.55,190.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:192.2,192.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:192.53,194.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:195.2,195.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:195.46,197.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:198.2,198.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:198.48,200.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:201.2,201.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:201.46,203.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:204.2,204.43 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:204.43,206.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:206.17,208.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:209.3,209.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:212.2,212.66 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:212.66,214.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:215.2,215.67 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:215.67,217.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:218.2,218.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:218.56,220.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:222.2,222.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:222.50,223.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:223.52,225.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:227.3,227.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:227.51,229.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:232.2,232.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:235.118,236.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:236.23,239.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:241.2,243.28 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:243.28,246.19 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:247.29,249.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:249.18,252.13 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:254.29,256.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:256.18,259.13 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:261.29,263.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:263.18,266.13 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:270.3,270.15 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:270.15,272.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:275.2,275.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:275.21,276.149 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:276.149,278.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:281.2,281.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:285.42,287.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:289.139,291.82 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:291.82,293.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:293.17,295.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:296.3,297.13 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:299.2,299.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:302.137,303.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:303.61,304.83 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:304.83,306.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:309.2,313.61 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:313.61,318.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:319.2,319.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:319.49,324.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:325.2,326.91 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:326.91,331.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:332.2,333.203 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:333.203,338.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:339.2,340.206 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:340.206,345.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:346.2,347.173 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:347.173,352.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:354.2,354.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:354.71,359.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:360.2,360.81 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:360.81,364.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:367.2,367.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:367.38,370.74 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:370.74,375.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:377.3,378.78 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:378.78,383.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:384.3,385.70 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:385.70,390.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:391.3,392.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:392.54,397.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:398.3,399.60 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:399.60,404.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:405.3,406.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:406.42,411.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:414.2,414.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:414.16,419.83 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:419.83,421.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:421.18,423.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:424.4,425.14 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:427.3,427.20 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:430.2,431.16 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:434.105,436.82 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:436.82,438.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:438.17,440.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:441.3,442.13 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:444.2,444.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:447.82,448.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:448.22,450.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:451.2,451.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:451.51,453.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:453.18,456.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:458.3,460.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:460.46,463.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:465.3,465.44 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:465.44,468.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:470.2,470.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:473.34,475.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:477.79,478.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:478.27,479.43 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:479.43,480.60 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:480.60,483.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:486.2,486.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:489.70,490.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:490.46,492.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:493.2,493.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:493.48,495.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:497.2,497.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:497.48,498.32 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:498.32,500.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:501.3,501.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:504.2,504.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:504.25,506.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:508.2,508.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:21.53,26.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:33.112,34.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:34.54,37.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:39.2,39.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:39.48,41.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:41.17,43.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:44.3,44.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:45.8,45.53 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:45.53,47.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:49.2,49.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:49.50,52.17 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:52.17,54.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:57.2,68.52 7 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:68.52,70.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:72.2,72.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:72.58,74.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:76.2,76.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:79.113,81.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:83.101,84.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:84.38,86.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:89.2,89.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:89.54,91.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:95.2,106.43 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:110.103,111.78 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:111.78,113.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:115.2,117.42 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:117.42,119.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:119.8,122.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:124.2,125.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:125.16,127.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:129.2,129.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:132.103,133.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:133.52,135.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:138.2,140.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:140.16,142.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:144.2,146.45 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:149.113,156.115 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:156.115,159.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:161.2,162.113 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:162.113,165.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:168.2,169.105 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:169.105,172.75 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:172.75,174.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:177.2,177.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:177.61,179.63 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:179.63,180.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:180.58,182.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:182.53,184.11 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:188.3,188.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:188.13,191.70 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:191.70,193.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:194.4,194.9 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:198.2,199.121 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:199.121,206.66 5 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:206.66,208.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:209.3,209.67 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:209.67,211.31 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:211.31,213.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:215.3,215.66 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:215.66,216.31 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:216.31,218.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:221.3,222.96 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:222.96,224.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:227.2,228.113 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:228.113,231.79 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:231.79,233.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:236.2,237.73 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:237.73,241.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:243.2,243.77 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:243.77,245.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:247.2,247.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:247.48,248.75 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:248.75,250.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:253.2,253.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:256.129,262.46 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:262.46,264.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:266.2,266.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:266.23,267.107 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:267.107,270.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:273.2,273.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:273.25,275.31 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:275.31,277.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:278.3,278.109 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:278.109,281.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:284.2,284.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:287.117,288.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:288.55,290.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:292.2,294.136 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:294.136,297.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:299.2,299.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:302.123,303.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:303.55,305.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:307.2,309.54 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:309.54,310.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:310.33,311.44 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:311.44,313.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:317.2,317.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:317.27,319.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:320.2,320.136 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:323.130,325.59 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:325.59,327.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:330.2,331.68 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:331.68,333.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:337.2,337.76 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:337.76,341.110 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:341.110,343.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:347.2,347.75 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:347.75,351.109 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:351.109,353.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:356.2,356.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:360.9,361.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:361.49,363.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:365.2,365.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:365.42,367.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:369.2,369.43 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:369.43,371.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:371.17,373.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:374.3,374.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:377.2,377.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:377.53,379.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:381.2,381.63 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:381.63,385.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:387.2,387.67 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:387.67,392.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:394.2,395.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:398.73,401.29 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:401.29,403.21 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:403.21,405.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:406.3,411.21 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:411.21,413.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:415.3,415.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:415.21,417.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:418.3,418.29 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:421.2,421.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:421.19,423.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:424.2,424.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:32.121,38.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:40.137,41.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:41.18,43.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:44.2,44.83 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:44.83,47.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:49.2,57.16 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:57.16,59.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:60.2,62.103 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:62.103,65.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:67.2,68.25 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:68.25,70.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:70.17,72.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:74.3,74.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:74.38,75.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:75.65,77.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:78.4,78.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:82.2,82.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:82.55,83.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:83.52,85.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:86.3,87.60 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:90.2,91.25 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:91.25,93.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:95.2,96.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:96.16,98.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:100.2,100.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:100.37,102.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:104.2,104.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:104.25,105.82 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:105.82,107.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:107.9,109.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:109.18,111.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:112.4,112.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:112.39,114.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:118.2,119.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:119.16,121.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:123.2,123.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:123.25,125.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:125.17,127.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:130.2,130.20 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:133.123,135.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:135.16,137.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:139.2,139.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:139.55,141.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:143.2,144.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:144.16,146.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:146.18,148.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:149.4,150.14 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:152.16,153.67 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:153.67,155.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:156.4,156.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:160.2,160.81 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:160.81,161.82 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:161.82,162.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:162.39,163.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:163.69,165.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:166.5,166.15 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:171.2,171.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:174.124,176.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:176.51,178.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:178.17,180.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:181.3,181.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:186.2,186.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:186.55,187.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:187.49,190.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:192.3,192.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:192.56,194.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:195.3,198.55 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:198.55,201.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:204.3,205.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:205.54,207.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:207.18,209.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:214.2,214.97 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:214.97,215.128 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:215.128,218.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:221.2,221.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:224.125,225.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:225.48,227.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:227.17,229.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:230.3,230.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:230.51,232.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:232.18,235.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:237.4,238.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:238.18,241.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:242.4,242.118 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:243.9,245.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:245.18,248.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:249.4,251.45 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:254.3,254.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:256.2,257.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:260.168,264.36 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:264.36,268.48 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:268.48,270.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:271.3,271.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:271.42,274.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:274.61,277.10 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:280.4,281.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:281.65,285.10 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:289.3,289.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:289.33,292.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:296.3,296.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:296.12,299.66 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:299.66,300.44 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:300.44,303.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:304.5,307.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:308.10,315.5 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:319.3,319.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:319.13,321.48 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:321.48,323.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:324.4,330.37 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:334.2,334.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:337.156,338.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:338.31,340.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:342.2,343.38 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:346.158,350.38 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:350.38,351.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:352.29,353.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:354.11,355.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:355.37,358.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:358.10,360.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:364.2,369.29 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:369.29,372.13 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:372.13,375.25 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:375.25,379.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:383.2,383.36 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:383.36,385.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:385.19,388.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:390.3,391.24 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:391.24,392.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:394.3,394.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:394.26,396.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:397.3,400.13 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:400.13,403.25 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:403.25,407.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:411.2,413.27 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:413.27,417.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:418.2,418.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:421.133,425.33 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:425.33,426.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:426.45,427.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:429.3,429.81 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:429.81,431.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:435.2,435.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:435.37,437.37 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:437.37,438.77 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:438.77,441.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:444.3,444.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:444.13,445.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:445.49,446.93 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:446.93,448.14 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:452.4,456.6 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:460.2,460.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:460.33,462.35 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:462.35,463.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:463.71,470.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:474.3,474.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:474.13,481.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:484.2,484.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:487.114,490.40 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:490.40,492.40 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:492.40,493.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:493.42,495.10 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:500.3,500.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:500.13,503.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:503.52,504.69 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:504.69,506.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:507.5,507.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:511.4,511.111 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:511.111,515.38 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:515.38,516.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:516.31,518.7 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:520.5,520.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:520.21,521.71 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:521.71,523.7 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:525.5,525.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:528.4,532.6 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:536.2,537.38 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:540.88,541.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:541.25,544.60 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:544.60,546.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:549.2,549.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:549.50,551.63 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:551.63,552.35 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:552.35,554.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:556.3,556.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:556.13,559.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:562.2,562.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:565.90,566.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:566.25,567.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:567.48,570.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:572.2,572.80 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:576.65,577.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:577.25,579.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:580.2,580.61 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:33.108,39.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:41.74,42.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:43.18,44.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:45.19,46.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:48.2,48.29 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:51.132,53.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:58.97,64.51 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:64.51,65.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:65.55,67.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:68.3,68.59 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:68.59,70.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:71.3,71.20 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:73.2,73.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:73.64,75.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:76.2,76.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:76.64,78.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:79.2,79.63 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:79.63,81.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:83.2,83.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:89.98,96.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:96.16,98.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:99.2,99.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:99.54,101.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:103.2,103.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:103.64,105.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:107.2,107.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:107.65,109.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:111.2,111.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:43.64,45.16 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:45.16,47.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:48.2,48.29 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:51.91,64.16 5 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:64.16,66.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:67.2,69.19 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:77.57,79.2 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:81.54,98.16 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:98.16,100.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:102.2,103.92 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:103.92,105.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:107.2,107.67 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:107.67,110.120 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:110.120,112.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:113.8,116.115 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:116.115,118.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:121.2,122.106 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:122.106,124.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:126.2,126.48 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:147.108,149.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:151.86,160.16 7 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:160.16,161.32 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:161.32,164.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:165.3,165.90 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:168.2,183.64 6 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:183.64,185.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:185.8,187.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:189.2,191.49 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:191.49,193.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:195.2,198.24 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:198.24,201.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:203.2,203.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:206.90,208.62 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:208.62,210.84 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:210.84,215.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:217.3,217.63 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:217.63,221.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:225.3,225.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:225.71,229.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:231.3,231.110 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:231.110,235.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:237.2,238.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:241.89,243.100 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:243.100,247.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:249.2,250.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:250.16,255.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:257.2,257.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:257.61,261.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:263.2,263.80 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:263.80,267.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:269.2,269.68 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:269.68,273.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:275.2,278.40 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:278.40,280.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:282.2,282.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:285.138,288.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:288.16,290.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:291.2,292.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:292.16,294.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:295.2,299.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:299.16,301.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:302.2,302.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:305.132,308.15 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:308.15,310.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:314.2,314.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:314.33,319.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:323.2,323.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:323.61,333.32 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:333.32,337.22 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:337.22,339.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:340.5,344.22 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:344.22,346.6 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:351.5,351.37 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:351.37,355.6 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:358.5,358.37 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:358.37,361.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:362.5,364.77 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:368.3,368.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:370.2,370.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:374.98,380.61 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:380.61,389.32 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:389.32,393.19 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:393.19,395.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:396.5,400.19 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:400.19,402.6 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:407.5,407.34 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:407.34,410.6 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:413.5,413.34 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:413.34,416.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:417.5,419.74 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:424.2,424.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:428.77,430.27 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:430.27,432.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:433.2,435.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:435.16,437.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:438.2,438.106 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:438.106,440.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:441.2,441.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:444.67,447.58 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:447.58,450.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:451.2,451.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:451.61,454.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:455.2,455.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:455.16,456.107 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:456.107,458.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:460.2,460.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:463.115,467.25 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:467.25,468.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:468.39,469.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:469.26,471.13 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:473.4,474.36 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:474.36,475.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:478.4,480.18 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:480.18,483.34 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:483.34,485.14 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:487.5,488.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:491.4,493.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:493.18,496.34 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:496.34,498.14 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:500.5,501.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:503.4,503.26 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:506.2,506.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:36.104,43.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:43.16,45.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:47.2,48.21 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:71.120,75.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:75.16,77.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:79.2,80.36 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:80.36,87.48 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:87.48,89.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:91.3,91.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:91.28,93.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:93.9,100.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:101.3,102.74 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:102.74,104.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:105.3,106.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:106.17,108.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:109.3,110.73 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:113.2,114.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:114.16,116.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:118.2,120.12 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:123.166,125.25 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:125.25,126.32 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:126.32,127.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:127.46,129.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:133.2,134.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:134.19,136.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:137.2,138.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:138.16,140.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:142.2,142.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:142.25,143.29 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:143.29,144.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:144.58,146.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:146.12,148.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:149.5,149.35 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:154.2,154.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:157.116,159.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:159.16,161.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:163.2,164.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:164.16,166.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:168.2,168.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:168.33,170.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:170.17,172.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:173.3,173.16 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:173.16,175.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:176.3,177.24 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:180.2,180.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:180.21,181.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:181.33,182.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:182.62,188.5 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:191.2,191.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:194.121,196.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:196.16,198.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:199.2,200.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:203.166,204.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:204.23,207.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:208.2,210.101 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:210.101,213.21 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:214.52,215.74 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:216.32,217.62 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:218.32,219.65 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:221.3,221.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:223.2,223.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:226.170,228.22 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:228.22,234.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:235.2,235.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:235.16,237.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:238.2,238.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:238.28,240.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:240.17,242.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:245.2,245.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:247.113,249.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:249.16,251.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:252.2,252.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:255.100,257.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:259.124,262.26 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:262.26,267.54 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:267.54,272.4 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:273.3,274.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:274.58,279.4 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:280.3,281.96 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:281.96,286.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:287.3,288.66 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:288.66,293.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:294.3,295.98 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:295.98,300.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:302.3,302.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:302.37,303.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:303.39,308.16 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:311.4,313.93 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:313.93,318.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:319.4,320.75 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:320.75,325.5 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:326.4,327.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:327.71,332.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:333.4,334.59 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:334.59,339.5 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:340.4,341.63 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:341.63,346.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:347.4,348.77 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:348.77,353.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:354.4,355.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:355.65,360.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:361.4,362.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:362.61,367.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:368.4,369.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:369.55,374.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:375.4,376.75 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:376.75,381.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:382.4,383.89 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:383.89,388.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:392.3,392.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:392.17,395.77 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:395.77,397.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:399.8,402.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:404.2,404.76 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:404.76,406.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:408.2,408.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:411.131,413.56 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:413.56,417.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:419.2,424.18 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:424.18,425.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:425.65,427.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:429.2,429.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:429.18,430.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:430.69,432.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:434.2,434.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:434.21,435.72 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:435.72,437.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:440.2,440.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:444.42,447.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:447.34,449.30 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:449.30,452.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:453.4,453.75 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:459.42,462.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:462.34,464.30 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:464.30,467.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:468.4,468.78 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:474.45,477.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:477.34,479.30 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:479.30,482.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:483.4,483.81 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:487.189,493.69 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:493.69,496.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:496.17,498.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:499.3,499.17 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:499.17,501.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:501.18,503.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:504.4,504.23 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:504.23,506.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:507.4,507.31 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:507.31,509.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:510.4,512.27 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:515.3,515.53 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:515.53,517.38 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:517.38,520.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:521.4,521.18 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:525.2,526.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:526.16,528.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:530.2,530.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:531.31,534.17 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:534.17,536.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:537.33,540.17 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:540.17,542.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:543.35,546.17 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:546.17,548.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:549.10,550.90 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:553.2,553.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:553.24,560.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:562.2,564.45 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:567.201,568.74 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:568.74,570.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:571.2,571.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:574.199,580.44 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:580.44,582.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:583.2,583.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:583.50,585.51 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:585.51,587.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:587.9,588.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:588.31,589.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:589.38,591.11 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:594.4,594.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:594.24,596.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:600.3,600.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:600.37,611.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:613.3,613.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:613.45,615.61 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:615.61,616.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:619.4,622.18 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:622.18,623.31 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:623.31,628.14 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:630.5,630.27 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:633.4,633.69 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:633.69,634.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:637.4,637.41 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:637.41,639.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:642.4,651.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:654.2,654.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:657.204,666.41 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:666.41,668.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:670.2,670.47 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:670.47,672.51 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:672.51,674.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:674.9,675.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:675.31,677.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:677.56,678.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:678.23,680.7 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:681.6,681.11 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:684.4,684.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:684.24,686.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:690.3,690.35 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:690.35,692.70 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:692.70,693.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:696.4,696.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:696.38,697.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:697.39,698.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:701.5,701.59 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:701.59,713.14 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:717.5,717.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:717.58,718.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:721.5,724.19 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:724.19,725.32 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:725.32,730.15 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:732.6,732.28 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:735.5,737.70 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:737.70,738.14 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:741.5,741.42 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:741.42,743.14 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:747.5,757.7 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:762.2,762.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:765.53,766.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:766.13,768.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:769.2,769.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:773.41,774.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:774.24,776.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:778.2,779.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:779.16,781.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:783.2,783.77 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:787.65,788.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:788.28,790.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:792.2,800.39 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:800.39,801.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:801.30,804.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:805.3,806.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:806.18,808.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:812.3,812.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:812.52,814.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:817.3,817.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:817.49,819.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:822.3,822.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:822.55,824.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:824.18,826.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:827.4,829.46 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:830.9,832.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:832.18,835.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:836.4,838.47 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:842.3,843.45 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:847.2,847.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:847.27,850.17 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:850.17,852.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:855.2,861.43 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:864.94,867.35 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:867.35,868.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:868.45,869.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:871.3,872.47 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:874.2,874.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:880.41,887.40 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:887.40,888.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:888.50,890.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:893.3,899.55 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:899.55,901.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:901.18,903.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:904.4,906.46 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:907.9,909.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:909.18,912.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:913.4,915.47 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:918.3,918.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:922.2,922.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:922.33,923.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:923.24,926.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:927.3,928.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:928.18,931.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:935.3,935.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:935.52,937.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:941.2,941.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:941.27,943.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:943.17,945.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:948.2,950.81 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:953.117,957.27 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:957.27,958.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:958.33,959.44 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:959.44,961.10 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:966.2,966.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:966.27,968.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:970.2,970.107 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:975.41,976.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:976.51,977.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:977.27,980.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:981.3,981.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:984.2,984.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:984.26,987.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:988.2,988.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:991.132,993.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:993.19,995.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:997.2,997.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1009.120,1010.76 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1010.76,1011.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1011.27,1013.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1014.3,1014.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1018.2,1019.29 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1019.29,1021.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1022.2,1022.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1022.26,1024.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1025.2,1025.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1036.32,1037.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1037.24,1039.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1041.2,1041.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1041.17,1042.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1042.27,1044.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1045.3,1045.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1048.2,1048.76 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1048.76,1050.14 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1050.14,1052.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1054.3,1054.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1054.27,1056.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1057.3,1057.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1061.2,1062.29 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1062.29,1064.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1065.2,1065.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1065.26,1067.29 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1067.29,1069.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1071.2,1071.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1074.113,1076.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1076.34,1077.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1078.19,1079.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1080.22,1081.42 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1083.8,1085.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1086.2,1094.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1097.117,1108.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1110.65,1121.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1123.106,1124.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1124.48,1125.98 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1125.98,1127.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1127.9,1127.111 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1127.111,1129.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1129.9,1131.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1134.2,1134.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1134.48,1136.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1138.2,1138.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1138.55,1140.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1140.17,1142.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1143.3,1143.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1146.2,1148.49 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1148.49,1151.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1153.2,1153.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1153.62,1155.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1158.2,1158.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1158.48,1160.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1160.34,1162.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1162.9,1164.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1165.3,1167.44 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1167.44,1168.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1168.50,1170.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1171.4,1171.57 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1171.57,1173.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1173.19,1175.6 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1176.5,1176.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1178.4,1178.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1178.51,1180.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1180.19,1182.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1183.5,1183.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1185.4,1185.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1185.53,1187.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1187.19,1189.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1190.5,1190.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1192.4,1192.60 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1192.60,1194.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1194.19,1196.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1197.5,1197.83 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1199.4,1199.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1199.54,1201.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1201.19,1203.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1204.5,1204.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1206.4,1209.54 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1209.54,1211.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1213.3,1213.43 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1216.2,1216.86 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1216.86,1218.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1219.2,1219.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1223.92,1231.35 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1231.35,1232.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1232.22,1233.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1235.3,1236.35 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1236.35,1237.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1237.27,1239.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1241.3,1241.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1241.13,1243.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1246.2,1246.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1246.34,1248.36 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1248.36,1249.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1249.27,1251.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1253.3,1253.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1253.13,1255.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1258.2,1258.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1258.34,1259.36 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1259.36,1260.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1260.27,1261.114 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1261.114,1263.6 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1268.2,1268.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1272.58,1273.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1273.34,1275.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1277.2,1277.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1278.30,1279.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1280.30,1281.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1282.29,1283.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1284.10,1286.15 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1290.68,1291.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1291.30,1293.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1294.2,1294.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1297.61,1298.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1298.22,1300.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1301.2,1302.51 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1302.51,1304.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1304.18,1307.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1308.3,1308.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1310.2,1310.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1313.104,1315.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1315.16,1317.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1318.2,1320.34 1 1 diff --git a/pkg/controller/service/nlbv2/coverage-server-groups b/pkg/controller/service/nlbv2/coverage-server-groups new file mode 100644 index 000000000..0f51730e6 --- /dev/null +++ b/pkg/controller/service/nlbv2/coverage-server-groups @@ -0,0 +1,1204 @@ +mode: set +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:27.106,29.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:37.127,39.24 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:39.24,42.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:45.127,49.63 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:49.63,52.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:55.128,58.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:60.130,62.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:64.123,72.2 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:74.81,75.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:75.56,77.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:79.2,79.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:79.54,89.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:91.2,91.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:91.30,95.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:97.2,97.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:97.64,108.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:110.2,110.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:110.50,120.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:122.2,122.94 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:122.94,133.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:135.2,135.14 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:138.43,139.32 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:139.32,141.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:144.2,144.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:144.58,147.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:148.2,148.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:152.130,157.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:166.128,168.49 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:168.49,171.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:174.128,179.48 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:179.48,184.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:187.128,189.49 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:189.49,192.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:195.131,197.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:199.128,207.2 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:209.75,210.15 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:210.15,212.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:214.2,214.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:214.30,216.82 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:216.82,218.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:221.2,227.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:227.16,228.33 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:228.33,230.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:231.3,231.15 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:234.2,234.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:234.26,239.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:240.2,240.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:244.115,249.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:258.124,260.42 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:260.42,263.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:266.124,270.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:270.16,271.75 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:271.75,273.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:276.3,276.40 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:276.40,279.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:283.124,285.42 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:285.42,288.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:291.127,293.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:295.111,304.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:304.16,308.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:310.2,310.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:310.31,311.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:311.26,312.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:314.3,314.40 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:314.40,315.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:317.3,324.47 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:328.96,329.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:329.34,331.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:333.2,333.80 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:333.80,335.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:337.2,337.82 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:337.82,339.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:341.2,346.45 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:346.45,350.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:352.2,352.14 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:356.133,361.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:370.133,372.54 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:372.54,375.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:378.133,383.41 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:383.41,388.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:391.133,393.54 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:393.54,396.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:399.136,401.2 0 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:403.154,405.9 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:405.9,407.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:409.2,416.96 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:419.91,420.15 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:420.15,422.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:424.2,425.9 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:425.9,427.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:429.2,435.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:435.16,436.33 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:436.33,439.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:440.3,440.15 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:443.2,443.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:443.26,448.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:449.2,449.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:452.74,454.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:456.54,457.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:457.69,459.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:460.2,460.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:460.62,466.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:467.2,467.40 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:467.40,469.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:470.2,470.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:473.55,476.20 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:476.20,477.84 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:477.84,479.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:481.2,481.20 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:481.20,482.84 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:482.84,484.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:486.2,486.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:486.52,492.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:494.2,494.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:497.76,498.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:498.28,502.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:503.2,503.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:503.25,504.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:504.19,509.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:512.2,512.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:516.50,517.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:517.39,519.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:521.2,521.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:521.33,524.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:525.2,525.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:525.31,528.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/event_handler.go:529.2,529.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:29.63,33.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:55.74,56.51 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:56.51,58.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:58.18,61.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:63.3,63.44 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:63.44,65.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:67.2,67.16 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:70.78,71.51 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:71.51,73.19 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:73.19,76.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:77.3,77.44 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:77.44,79.28 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:79.28,82.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:84.4,85.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:85.18,87.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:88.4,89.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:89.18,91.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:92.4,92.28 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:92.28,94.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:95.4,95.40 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:95.40,97.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:99.4,99.48 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:102.2,102.18 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:105.117,106.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:106.49,108.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:108.17,110.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:111.3,111.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:113.2,113.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:116.118,118.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:118.16,120.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:121.2,122.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:126.61,133.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:133.16,135.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:136.2,138.57 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:138.57,139.47 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:139.47,141.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:142.3,143.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:143.17,145.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:146.3,146.37 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:146.37,150.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:153.2,167.32 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:167.32,169.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:169.8,171.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:173.2,173.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:173.69,175.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:175.17,177.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:178.3,178.41 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:181.2,181.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:181.51,183.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:183.17,185.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:186.3,186.44 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:188.2,188.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:188.55,190.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:192.2,192.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:192.53,194.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:195.2,195.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:195.46,197.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:198.2,198.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:198.48,200.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:201.2,201.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:201.46,203.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:204.2,204.43 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:204.43,206.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:206.17,208.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:209.3,209.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:212.2,212.66 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:212.66,214.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:215.2,215.67 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:215.67,217.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:218.2,218.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:218.56,220.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:222.2,222.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:222.50,223.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:223.52,225.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:227.3,227.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:227.51,229.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:232.2,232.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:235.118,236.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:236.23,239.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:241.2,243.28 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:243.28,246.19 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:247.29,249.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:249.18,252.13 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:254.29,256.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:256.18,259.13 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:261.29,263.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:263.18,266.13 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:270.3,270.15 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:270.15,272.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:275.2,275.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:275.21,276.149 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:276.149,278.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:281.2,281.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:285.42,287.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:289.139,291.82 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:291.82,293.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:293.17,295.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:296.3,297.13 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:299.2,299.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:302.137,303.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:303.61,304.83 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:304.83,306.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:309.2,313.61 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:313.61,318.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:319.2,319.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:319.49,324.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:325.2,326.91 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:326.91,331.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:332.2,333.203 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:333.203,338.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:339.2,340.206 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:340.206,345.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:346.2,347.173 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:347.173,352.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:354.2,354.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:354.71,359.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:360.2,360.81 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:360.81,364.3 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:367.2,367.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:367.38,370.74 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:370.74,375.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:377.3,378.78 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:378.78,383.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:384.3,385.70 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:385.70,390.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:391.3,392.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:392.54,397.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:398.3,399.60 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:399.60,404.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:405.3,406.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:406.42,411.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:414.2,414.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:414.16,419.83 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:419.83,421.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:421.18,423.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:424.4,425.14 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:427.3,427.20 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:430.2,431.16 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:434.105,436.82 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:436.82,438.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:438.17,440.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:441.3,442.13 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:444.2,444.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:447.82,448.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:448.22,450.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:451.2,451.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:451.51,453.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:453.18,456.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:458.3,460.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:460.46,463.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:465.3,465.44 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:465.44,468.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:470.2,470.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:473.34,475.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:477.79,478.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:478.27,479.43 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:479.43,480.60 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:480.60,483.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:486.2,486.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:489.70,490.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:490.46,492.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:493.2,493.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:493.48,495.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:497.2,497.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:497.48,498.32 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:498.32,500.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:501.3,501.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:504.2,504.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:504.25,506.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/listeners.go:508.2,508.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:21.53,26.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:33.112,34.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:34.54,37.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:39.2,39.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:39.48,41.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:41.17,43.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:44.3,44.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:45.8,45.53 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:45.53,47.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:49.2,49.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:49.50,52.17 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:52.17,54.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:57.2,68.52 7 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:68.52,70.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:72.2,72.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:72.58,74.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:76.2,76.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:79.113,81.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:83.101,84.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:84.38,86.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:89.2,89.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:89.54,91.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:95.2,106.43 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:110.103,111.78 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:111.78,113.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:115.2,117.42 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:117.42,119.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:119.8,122.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:124.2,125.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:125.16,127.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:129.2,129.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:132.103,133.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:133.52,135.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:138.2,140.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:140.16,142.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:144.2,146.45 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:149.113,156.115 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:156.115,159.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:161.2,162.113 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:162.113,165.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:168.2,169.105 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:169.105,172.75 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:172.75,174.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:177.2,177.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:177.61,179.63 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:179.63,180.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:180.58,182.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:182.53,184.11 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:188.3,188.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:188.13,191.70 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:191.70,193.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:194.4,194.9 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:198.2,199.121 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:199.121,206.66 5 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:206.66,208.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:209.3,209.67 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:209.67,211.31 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:211.31,213.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:215.3,215.66 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:215.66,216.31 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:216.31,218.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:221.3,222.96 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:222.96,224.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:227.2,228.113 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:228.113,231.79 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:231.79,233.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:236.2,237.73 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:237.73,241.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:243.2,243.77 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:243.77,245.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:247.2,247.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:247.48,248.75 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:248.75,250.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:253.2,253.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:256.129,262.46 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:262.46,264.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:266.2,266.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:266.23,267.107 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:267.107,270.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:273.2,273.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:273.25,275.31 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:275.31,277.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:278.3,278.109 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:278.109,281.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:284.2,284.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:287.117,288.55 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:288.55,290.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:292.2,294.136 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:294.136,297.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:299.2,299.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:302.123,303.55 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:303.55,305.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:307.2,309.54 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:309.54,310.33 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:310.33,311.44 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:311.44,313.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:317.2,317.27 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:317.27,319.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:320.2,320.136 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:323.130,325.59 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:325.59,327.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:330.2,331.68 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:331.68,333.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:337.2,337.76 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:337.76,341.110 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:341.110,343.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:347.2,347.75 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:347.75,351.109 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:351.109,353.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:356.2,356.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:360.9,361.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:361.49,363.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:365.2,365.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:365.42,367.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:369.2,369.43 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:369.43,371.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:371.17,373.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:374.3,374.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:377.2,377.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:377.53,379.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:381.2,381.63 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:381.63,385.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:387.2,387.67 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:387.67,392.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:394.2,395.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:398.73,401.29 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:401.29,403.21 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:403.21,405.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:406.3,411.21 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:411.21,413.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:415.3,415.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:415.21,417.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:418.3,418.29 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:421.2,421.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:421.19,423.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/loadbalancer.go:424.2,424.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:32.121,38.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:40.137,41.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:41.18,43.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:44.2,44.83 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:44.83,47.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:49.2,57.16 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:57.16,59.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:60.2,62.103 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:62.103,65.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:67.2,68.25 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:68.25,70.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:70.17,72.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:74.3,74.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:74.38,75.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:75.65,77.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:78.4,78.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:82.2,82.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:82.55,83.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:83.52,85.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:86.3,87.60 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:90.2,91.25 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:91.25,93.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:95.2,96.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:96.16,98.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:100.2,100.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:100.37,102.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:104.2,104.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:104.25,105.82 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:105.82,107.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:107.9,109.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:109.18,111.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:112.4,112.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:112.39,114.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:118.2,119.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:119.16,121.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:123.2,123.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:123.25,125.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:125.17,127.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:130.2,130.20 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:133.123,135.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:135.16,137.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:139.2,139.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:139.55,141.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:143.2,144.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:144.16,146.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:146.18,148.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:149.4,150.14 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:152.16,153.67 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:153.67,155.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:156.4,156.14 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:160.2,160.81 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:160.81,161.82 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:161.82,162.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:162.39,163.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:163.69,165.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:166.5,166.15 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:171.2,171.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:174.124,176.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:176.51,178.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:178.17,180.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:181.3,181.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:186.2,186.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:186.55,187.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:187.49,190.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:192.3,192.56 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:192.56,194.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:195.3,198.55 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:198.55,201.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:204.3,205.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:205.54,207.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:207.18,209.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:214.2,214.97 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:214.97,215.128 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:215.128,218.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:221.2,221.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:224.125,225.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:225.48,227.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:227.17,229.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:230.3,230.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:230.51,232.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:232.18,235.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:237.4,238.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:238.18,241.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:242.4,242.118 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:243.9,245.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:245.18,248.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:249.4,251.45 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:254.3,254.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:256.2,257.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:260.168,264.36 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:264.36,268.48 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:268.48,270.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:271.3,271.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:271.42,274.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:274.61,277.10 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:280.4,281.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:281.65,285.10 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:289.3,289.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:289.33,292.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:296.3,296.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:296.12,299.66 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:299.66,300.44 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:300.44,303.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:304.5,307.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:308.10,315.5 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:319.3,319.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:319.13,321.48 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:321.48,323.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:324.4,330.37 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:334.2,334.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:337.156,338.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:338.31,340.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:342.2,343.38 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:346.158,350.38 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:350.38,351.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:352.29,353.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:354.11,355.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:355.37,358.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:358.10,360.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:364.2,369.29 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:369.29,372.13 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:372.13,375.25 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:375.25,379.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:383.2,383.36 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:383.36,385.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:385.19,388.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:390.3,391.24 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:391.24,392.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:394.3,394.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:394.26,396.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:397.3,400.13 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:400.13,403.25 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:403.25,407.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:411.2,413.27 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:413.27,417.3 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:418.2,418.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:421.133,425.33 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:425.33,426.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:426.45,427.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:429.3,429.81 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:429.81,431.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:435.2,435.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:435.37,437.37 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:437.37,438.77 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:438.77,441.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:444.3,444.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:444.13,445.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:445.49,446.93 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:446.93,448.14 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:452.4,456.6 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:460.2,460.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:460.33,462.35 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:462.35,463.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:463.71,470.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:474.3,474.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:474.13,481.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:484.2,484.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:487.114,490.40 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:490.40,492.40 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:492.40,493.42 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:493.42,495.10 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:500.3,500.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:500.13,503.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:503.52,504.69 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:504.69,506.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:507.5,507.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:511.4,511.111 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:511.111,515.38 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:515.38,516.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:516.31,518.7 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:520.5,520.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:520.21,521.71 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:521.71,523.7 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:525.5,525.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:528.4,532.6 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:536.2,537.38 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:540.88,541.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:541.25,544.60 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:544.60,546.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:549.2,549.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:549.50,551.63 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:551.63,552.35 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:552.35,554.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:556.3,556.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:556.13,559.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:562.2,562.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:565.90,566.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:566.25,567.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:567.48,570.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:572.2,572.80 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:576.65,577.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:577.25,579.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_applier.go:580.2,580.61 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:33.108,39.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:41.74,42.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:43.18,44.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:45.19,46.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:48.2,48.29 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:51.132,53.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:58.97,64.51 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:64.51,65.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:65.55,67.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:68.3,68.59 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:68.59,70.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:71.3,71.20 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:73.2,73.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:73.64,75.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:76.2,76.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:76.64,78.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:79.2,79.63 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:79.63,81.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:83.2,83.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:89.98,96.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:96.16,98.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:99.2,99.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:99.54,101.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:103.2,103.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:103.64,105.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:107.2,107.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:107.65,109.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/model_builder.go:111.2,111.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:43.64,45.16 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:45.16,47.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:48.2,48.29 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:51.91,64.16 5 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:64.16,66.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:67.2,69.19 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:77.57,79.2 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:81.54,98.16 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:98.16,100.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:102.2,103.92 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:103.92,105.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:107.2,107.67 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:107.67,110.120 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:110.120,112.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:113.8,116.115 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:116.115,118.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:121.2,122.106 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:122.106,124.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:126.2,126.48 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:147.108,149.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:151.86,160.16 7 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:160.16,161.32 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:161.32,164.4 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:165.3,165.90 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:168.2,183.64 6 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:183.64,185.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:185.8,187.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:189.2,191.49 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:191.49,193.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:195.2,198.24 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:198.24,201.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:203.2,203.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:206.90,208.62 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:208.62,210.84 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:210.84,215.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:217.3,217.63 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:217.63,221.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:225.3,225.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:225.71,229.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:231.3,231.110 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:231.110,235.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:237.2,238.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:241.89,243.100 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:243.100,247.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:249.2,250.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:250.16,255.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:257.2,257.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:257.61,261.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:263.2,263.80 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:263.80,267.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:269.2,269.68 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:269.68,273.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:275.2,278.40 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:278.40,280.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:282.2,282.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:285.138,288.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:288.16,290.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:291.2,292.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:292.16,294.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:295.2,299.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:299.16,301.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:302.2,302.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:305.132,308.15 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:308.15,310.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:314.2,314.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:314.33,319.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:323.2,323.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:323.61,333.32 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:333.32,337.22 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:337.22,339.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:340.5,344.22 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:344.22,346.6 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:351.5,351.37 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:351.37,355.6 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:358.5,358.37 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:358.37,361.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:362.5,364.77 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:368.3,368.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:370.2,370.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:374.98,380.61 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:380.61,389.32 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:389.32,393.19 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:393.19,395.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:396.5,400.19 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:400.19,402.6 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:407.5,407.34 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:407.34,410.6 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:413.5,413.34 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:413.34,416.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:417.5,419.74 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:424.2,424.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:428.77,430.27 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:430.27,432.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:433.2,435.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:435.16,437.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:438.2,438.106 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:438.106,440.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:441.2,441.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:444.67,447.58 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:447.58,450.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:451.2,451.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:451.61,454.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:455.2,455.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:455.16,456.107 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:456.107,458.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:460.2,460.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:463.115,467.25 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:467.25,468.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:468.39,469.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:469.26,471.13 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:473.4,474.36 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:474.36,475.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:478.4,480.18 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:480.18,483.34 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:483.34,485.14 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:487.5,488.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:491.4,493.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:493.18,496.34 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:496.34,498.14 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:500.5,501.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:503.4,503.26 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/nlb_controller.go:506.2,506.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:36.104,43.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:43.16,45.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:47.2,48.21 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:71.120,75.16 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:75.16,77.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:79.2,80.36 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:80.36,87.48 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:87.48,89.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:91.3,91.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:91.28,93.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:93.9,100.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:101.3,102.74 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:102.74,104.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:105.3,106.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:106.17,108.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:109.3,110.73 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:113.2,114.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:114.16,116.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:118.2,120.12 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:123.166,125.25 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:125.25,126.32 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:126.32,127.46 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:127.46,129.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:133.2,134.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:134.19,136.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:137.2,138.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:138.16,140.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:142.2,142.25 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:142.25,143.29 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:143.29,144.58 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:144.58,146.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:146.12,148.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:149.5,149.35 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:154.2,154.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:157.116,159.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:159.16,161.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:163.2,164.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:164.16,166.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:168.2,168.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:168.33,170.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:170.17,172.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:173.3,173.16 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:173.16,175.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:176.3,177.24 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:180.2,180.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:180.21,181.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:181.33,182.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:182.62,188.5 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:191.2,191.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:194.121,196.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:196.16,198.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:199.2,200.12 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:203.166,204.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:204.23,207.3 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:208.2,210.101 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:210.101,213.21 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:214.52,215.74 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:216.32,217.62 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:218.32,219.65 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:221.3,221.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:223.2,223.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:226.170,228.22 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:228.22,234.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:235.2,235.16 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:235.16,237.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:238.2,238.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:238.28,240.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:240.17,242.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:245.2,245.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:247.113,249.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:249.16,251.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:252.2,252.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:255.100,257.2 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:259.124,262.26 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:262.26,267.54 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:267.54,272.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:273.3,274.58 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:274.58,279.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:280.3,281.96 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:281.96,286.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:287.3,288.66 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:288.66,293.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:294.3,295.98 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:295.98,300.4 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:302.3,302.37 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:302.37,303.39 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:303.39,308.16 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:311.4,313.93 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:313.93,318.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:319.4,320.75 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:320.75,325.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:326.4,327.71 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:327.71,332.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:333.4,334.59 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:334.59,339.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:340.4,341.63 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:341.63,346.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:347.4,348.77 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:348.77,353.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:354.4,355.65 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:355.65,360.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:361.4,362.61 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:362.61,367.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:368.4,369.55 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:369.55,374.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:375.4,376.75 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:376.75,381.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:382.4,383.89 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:383.89,388.5 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:392.3,392.17 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:392.17,395.77 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:395.77,397.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:399.8,402.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:404.2,404.76 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:404.76,406.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:408.2,408.38 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:411.131,413.56 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:413.56,417.3 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:419.2,424.18 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:424.18,425.65 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:425.65,427.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:429.2,429.18 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:429.18,430.69 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:430.69,432.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:434.2,434.21 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:434.21,435.72 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:435.72,437.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:440.2,440.38 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:444.42,447.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:447.34,449.30 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:449.30,452.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:453.4,453.75 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:459.42,462.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:462.34,464.30 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:464.30,467.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:468.4,468.78 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:474.45,477.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:477.34,479.30 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:479.30,482.5 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:483.4,483.81 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:487.189,493.69 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:493.69,496.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:496.17,498.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:499.3,499.17 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:499.17,501.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:501.18,503.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:504.4,504.23 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:504.23,506.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:507.4,507.31 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:507.31,509.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:510.4,512.27 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:515.3,515.53 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:515.53,517.38 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:517.38,520.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:521.4,521.18 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:525.2,526.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:526.16,528.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:530.2,530.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:531.31,534.17 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:534.17,536.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:537.33,540.17 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:540.17,542.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:543.35,546.17 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:546.17,548.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:549.10,550.90 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:553.2,553.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:553.24,560.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:562.2,564.45 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:567.201,568.74 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:568.74,570.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:571.2,571.61 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:574.199,580.44 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:580.44,582.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:583.2,583.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:583.50,585.51 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:585.51,587.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:587.9,588.31 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:588.31,589.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:589.38,591.11 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:594.4,594.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:594.24,596.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:600.3,600.37 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:600.37,611.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:613.3,613.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:613.45,615.61 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:615.61,616.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:619.4,622.18 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:622.18,623.31 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:623.31,628.14 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:630.5,630.27 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:633.4,633.69 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:633.69,634.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:637.4,637.41 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:637.41,639.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:642.4,651.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:654.2,654.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:657.204,666.41 5 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:666.41,668.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:670.2,670.47 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:670.47,672.51 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:672.51,674.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:674.9,675.31 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:675.31,677.56 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:677.56,678.23 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:678.23,680.7 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:681.6,681.11 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:684.4,684.24 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:684.24,686.13 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:690.3,690.35 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:690.35,692.70 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:692.70,693.13 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:696.4,696.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:696.38,697.39 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:697.39,698.14 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:701.5,701.59 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:701.59,713.14 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:717.5,717.58 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:717.58,718.14 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:721.5,724.19 4 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:724.19,725.32 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:725.32,730.15 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:732.6,732.28 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:735.5,737.70 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:737.70,738.14 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:741.5,741.42 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:741.42,743.14 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:747.5,757.7 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:762.2,762.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:765.53,766.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:766.13,768.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:769.2,769.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:773.41,774.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:774.24,776.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:778.2,779.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:779.16,781.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:783.2,783.77 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:787.65,788.28 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:788.28,790.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:792.2,800.39 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:800.39,801.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:801.30,804.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:805.3,806.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:806.18,808.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:812.3,812.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:812.52,814.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:817.3,817.49 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:817.49,819.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:822.3,822.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:822.55,824.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:824.18,826.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:827.4,829.46 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:830.9,832.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:832.18,835.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:836.4,838.47 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:842.3,843.45 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:847.2,847.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:847.27,850.17 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:850.17,852.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:855.2,861.43 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:864.94,867.35 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:867.35,868.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:868.45,869.12 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:871.3,872.47 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:874.2,874.21 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:880.41,887.40 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:887.40,888.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:888.50,890.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:893.3,899.55 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:899.55,901.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:901.18,903.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:904.4,906.46 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:907.9,909.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:909.18,912.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:913.4,915.47 3 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:918.3,918.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:922.2,922.33 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:922.33,923.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:923.24,926.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:927.3,928.18 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:928.18,931.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:935.3,935.52 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:935.52,937.12 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:941.2,941.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:941.27,943.17 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:943.17,945.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:948.2,950.81 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:953.117,957.27 3 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:957.27,958.33 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:958.33,959.44 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:959.44,961.10 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:966.2,966.27 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:966.27,968.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:970.2,970.107 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:975.41,976.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:976.51,977.27 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:977.27,980.4 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:981.3,981.23 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:984.2,984.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:984.26,987.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:988.2,988.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:991.132,993.19 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:993.19,995.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:997.2,997.53 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1009.120,1010.76 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1010.76,1011.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1011.27,1013.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1014.3,1014.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1018.2,1019.29 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1019.29,1021.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1022.2,1022.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1022.26,1024.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1025.2,1025.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1036.32,1037.24 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1037.24,1039.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1041.2,1041.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1041.17,1042.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1042.27,1044.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1045.3,1045.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1048.2,1048.76 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1048.76,1050.14 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1050.14,1052.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1054.3,1054.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1054.27,1056.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1057.3,1057.18 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1061.2,1062.29 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1062.29,1064.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1065.2,1065.26 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1065.26,1067.29 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1067.29,1069.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1071.2,1071.17 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1074.113,1076.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1076.34,1077.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1078.19,1079.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1080.22,1081.42 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1083.8,1085.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1086.2,1094.23 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1097.117,1108.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1110.65,1121.2 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1123.106,1124.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1124.48,1125.98 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1125.98,1127.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1127.9,1127.111 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1127.111,1129.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1129.9,1131.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1134.2,1134.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1134.48,1136.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1138.2,1138.55 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1138.55,1140.17 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1140.17,1142.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1143.3,1143.45 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1146.2,1148.49 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1148.49,1151.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1153.2,1153.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1153.62,1155.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1158.2,1158.48 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1158.48,1160.34 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1160.34,1162.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1162.9,1164.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1165.3,1167.44 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1167.44,1168.50 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1168.50,1170.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1171.4,1171.57 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1171.57,1173.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1173.19,1175.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1176.5,1176.64 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1178.4,1178.51 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1178.51,1180.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1180.19,1182.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1183.5,1183.65 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1185.4,1185.53 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1185.53,1187.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1187.19,1189.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1190.5,1190.69 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1192.4,1192.60 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1192.60,1194.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1194.19,1196.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1197.5,1197.83 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1199.4,1199.54 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1199.54,1201.19 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1201.19,1203.6 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1204.5,1204.71 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1206.4,1209.54 4 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1209.54,1211.5 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1213.3,1213.43 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1216.2,1216.86 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1216.86,1218.3 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1219.2,1219.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1223.92,1231.35 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1231.35,1232.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1232.22,1233.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1235.3,1236.35 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1236.35,1237.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1237.27,1239.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1241.3,1241.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1241.13,1243.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1246.2,1246.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1246.34,1248.36 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1248.36,1249.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1249.27,1251.5 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1253.3,1253.13 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1253.13,1255.4 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1258.2,1258.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1258.34,1259.36 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1259.36,1260.27 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1260.27,1261.114 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1261.114,1263.6 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1268.2,1268.38 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1272.58,1273.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1273.34,1275.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1277.2,1277.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1278.30,1279.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1280.30,1281.34 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1282.29,1283.62 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1284.10,1286.15 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1290.68,1291.30 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1291.30,1293.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1294.2,1294.12 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1297.61,1298.22 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1298.22,1300.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1301.2,1302.51 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1302.51,1304.18 2 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1304.18,1307.4 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1308.3,1308.27 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1310.2,1310.17 1 0 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1313.104,1315.16 2 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1315.16,1317.3 1 1 +k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/nlbv2/server_groups.go:1318.2,1320.34 1 0 diff --git a/pkg/controller/service/nlbv2/event_handler_test.go b/pkg/controller/service/nlbv2/event_handler_test.go index 20f6f8864..96a542274 100644 --- a/pkg/controller/service/nlbv2/event_handler_test.go +++ b/pkg/controller/service/nlbv2/event_handler_test.go @@ -2,18 +2,22 @@ package nlbv2 import ( "context" + "testing" + "time" + "github.com/alibabacloud-go/tea/tea" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" + "k8s.io/cloud-provider-alibaba-cloud/pkg/config" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" + "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" "sigs.k8s.io/controller-runtime/pkg/event" - "testing" - "time" ) func TestEnqueueRequestForServiceEvent(t *testing.T) { @@ -307,3 +311,149 @@ func Test_nodeSpecChanged(t *testing.T) { newN.Spec.Unschedulable = true assert.Equal(t, nodeSpecChanged(oldN, newN), true) } + +func Test_checkServiceAffected(t *testing.T) { + defaultEnabled := feature.DefaultMutableFeatureGate.Enabled(config.FilterServiceOnNodeChange) + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): true, + }) + assert.NoError(t, err) + defer func() { + err := feature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{ + string(config.FilterServiceOnNodeChange): defaultEnabled, + }) + assert.NoError(t, err) + }() + + kubeClient := getFakeKubeClient() + h := NewEnqueueRequestForNodeEvent(kubeClient, record.NewFakeRecorder(100)) + + node := &v1.Node{} + _ = kubeClient.Get(context.TODO(), types.NamespacedName{ + Name: NodeName, + }, node) + + tests := []struct { + name string + node *v1.Node + svc *v1.Service + expected bool + }{ + { + name: "ENI backend type", + node: node, + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotation.BackendType: "eni", + }, + }, + }, + expected: false, + }, + { + name: "Cluster traffic policy", + node: node, + svc: &v1.Service{ + Spec: v1.ServiceSpec{ + ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + }, + }, + expected: true, + }, + { + name: "Local traffic policy without annotations", + node: node, + svc: &v1.Service{ + Spec: v1.ServiceSpec{ + ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal, + }, + }, + expected: false, + }, + { + name: "Service with BackendLabel annotation", + node: node, + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotation.Annotation(annotation.BackendLabel): "app=nginx", + }, + }, + Spec: v1.ServiceSpec{ + ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal, + }, + }, + expected: true, + }, + { + name: "Service with RemoveUnscheduled annotation", + node: node, + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + annotation.Annotation(annotation.RemoveUnscheduled): "on", + }, + }, + Spec: v1.ServiceSpec{ + ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := h.checkServiceAffected(tt.node, tt.svc) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestEventHandlerNoopBranches(t *testing.T) { + ctx := context.TODO() + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + defer queue.ShutDown() + + kubeClient := getFakeKubeClient() + svcHandler := NewEnqueueRequestForServiceEvent(record.NewFakeRecorder(100)) + epHandler := NewEnqueueRequestForEndpointEvent(kubeClient, record.NewFakeRecorder(100)) + nodeHandler := NewEnqueueRequestForNodeEvent(kubeClient, record.NewFakeRecorder(100)) + esHandler := NewEnqueueRequestForEndpointSliceEvent(kubeClient, record.NewFakeRecorder(100)) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeLoadBalancer, + }, + } + ep := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + } + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: NodeName, + }, + } + es := &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + } + + svcHandler.Delete(ctx, event.DeleteEvent{Object: svc}, queue) + svcHandler.Generic(ctx, event.GenericEvent{Object: svc}, queue) + epHandler.Generic(ctx, event.GenericEvent{Object: ep}, queue) + nodeHandler.Generic(ctx, event.GenericEvent{Object: node}, queue) + esHandler.Generic(ctx, event.GenericEvent{Object: es}, queue) + + assert.Equal(t, 0, queue.Len()) +} diff --git a/pkg/controller/service/nlbv2/listeners_test.go b/pkg/controller/service/nlbv2/listeners_test.go index 5b089332b..5505bf796 100644 --- a/pkg/controller/service/nlbv2/listeners_test.go +++ b/pkg/controller/service/nlbv2/listeners_test.go @@ -1,10 +1,18 @@ package nlbv2 import ( + "context" "fmt" + "testing" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" + svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" nlbmodel "k8s.io/cloud-provider-alibaba-cloud/pkg/model/nlb" - "testing" + "k8s.io/cloud-provider-alibaba-cloud/pkg/util" ) func singlePort(p int32) *nlbmodel.ListenerAttribute { @@ -84,3 +92,335 @@ func TestIsListenerPortOverlapped(t *testing.T) { }) } } + +func TestServerGroup(t *testing.T) { + tests := []struct { + name string + annotation string + port v1.ServicePort + expected string + expectError bool + }{ + { + name: "single server group match", + annotation: "sg-123:443", + port: v1.ServicePort{ + Port: 443, + }, + expected: "sg-123", + expectError: false, + }, + { + name: "multiple server groups match first", + annotation: "sg-123:443,sg-456:80", + port: v1.ServicePort{ + Port: 443, + }, + expected: "sg-123", + expectError: false, + }, + { + name: "multiple server groups match second", + annotation: "sg-123:443,sg-456:80", + port: v1.ServicePort{ + Port: 80, + }, + expected: "sg-456", + expectError: false, + }, + { + name: "no match returns empty", + annotation: "sg-123:443,sg-456:80", + port: v1.ServicePort{ + Port: 8080, + }, + expected: "", + expectError: false, + }, + { + name: "empty annotation returns error", + annotation: "", + port: v1.ServicePort{ + Port: 443, + }, + expected: "", + expectError: true, + }, + { + name: "invalid format missing colon", + annotation: "sg-123", + port: v1.ServicePort{ + Port: 443, + }, + expected: "", + expectError: true, + }, + { + name: "empty server group id returns empty", + annotation: ":443", + port: v1.ServicePort{ + Port: 443, + }, + expected: "", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := serverGroup(tt.annotation, tt.port) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestListenerAdditionalCertificateHelpers(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + mgr := NewListenerManager(recon.cloud) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "listener-cert-svc", + Namespace: v1.NamespaceDefault, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeLoadBalancer, + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 443, + TargetPort: intstr.FromInt(443), + Protocol: v1.ProtocolTCP, + }, + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + err = mgr.SetListenerAdditionalCertificates(reqCtx, []*nlbmodel.ListenerAttribute{ + { + ListenerProtocol: nlbmodel.TCP, + ListenerId: "lis-tcp", + }, + { + ListenerProtocol: nlbmodel.TCPSSL, + ListenerId: "lis-tcpssl", + }, + }) + assert.NoError(t, err) + + assert.NoError(t, mgr.BatchAssociateAdditionalCertificates(context.TODO(), "lis-tcpssl", []string{"c-1"})) + assert.NoError(t, mgr.BatchDisassociateAdditionalCertificates(context.TODO(), "lis-tcpssl", []string{"c-1"})) + assert.True(t, needUpdateAfterListenerCreated(&nlbmodel.ListenerAttribute{ + AdditionalCertificateIds: []string{"c-1"}, + })) + assert.False(t, needUpdateAfterListenerCreated(&nlbmodel.ListenerAttribute{})) +} + +func TestCreateAndUpdateListener(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + mgr := NewListenerManager(recon.cloud) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "create-update-listener", + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + t.Run("listener id empty returns error", func(t *testing.T) { + err := mgr.CreateAndUpdateListener(reqCtx, "nlb-id", &nlbmodel.ListenerAttribute{ + ListenerId: "", + }) + assert.Error(t, err) + }) + + t.Run("listener id with additional certs", func(t *testing.T) { + err := mgr.CreateAndUpdateListener(reqCtx, "nlb-id", &nlbmodel.ListenerAttribute{ + ListenerId: "lis-id", + AdditionalCertificateIds: []string{"cert-a", "cert-b"}, + }) + assert.NoError(t, err) + }) +} + +func TestUpdateListenerAdditionalCertificates(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + mgr := NewListenerManager(recon.cloud) + + local := &nlbmodel.ListenerAttribute{ + ListenerId: "lis-id", + AdditionalCertificateIds: []string{"cert-1", "cert-2"}, + } + remote := &nlbmodel.ListenerAttribute{ + ListenerId: "lis-id", + AdditionalCertificateIds: []string{"cert-2", "cert-3"}, + } + + err = mgr.updateListenerAdditionalCertificates(context.TODO(), local, remote) + assert.NoError(t, err) +} + +func TestPortRange(t *testing.T) { + tests := []struct { + name string + annotation string + port v1.ServicePort + expectedStart int32 + expectedEnd int32 + expectError bool + }{ + { + name: "valid port range match", + annotation: "1000-2000:443", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 1000, + expectedEnd: 2000, + expectError: false, + }, + { + name: "multiple port ranges match first", + annotation: "1000-2000:443,3000-4000:80", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 1000, + expectedEnd: 2000, + expectError: false, + }, + { + name: "multiple port ranges match second", + annotation: "1000-2000:443,3000-4000:80", + port: v1.ServicePort{ + Port: 80, + }, + expectedStart: 3000, + expectedEnd: 4000, + expectError: false, + }, + { + name: "no match returns zero", + annotation: "1000-2000:443,3000-4000:80", + port: v1.ServicePort{ + Port: 8080, + }, + expectedStart: 0, + expectedEnd: 0, + expectError: false, + }, + { + name: "invalid format missing colon", + annotation: "1000-2000", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 0, + expectedEnd: 0, + expectError: true, + }, + { + name: "invalid format missing dash", + annotation: "1000:443", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 0, + expectedEnd: 0, + expectError: true, + }, + { + name: "invalid format start port not number", + annotation: "abc-2000:443", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 0, + expectedEnd: 0, + expectError: true, + }, + { + name: "invalid format end port not number", + annotation: "1000-xyz:443", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 0, + expectedEnd: 0, + expectError: true, + }, + { + name: "invalid format start port >= end port", + annotation: "2000-1000:443", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 0, + expectedEnd: 0, + expectError: true, + }, + { + name: "invalid format start port < 1", + annotation: "0-2000:443", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 0, + expectedEnd: 0, + expectError: true, + }, + { + name: "invalid format end port > 65535", + annotation: "1000-65536:443", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 0, + expectedEnd: 0, + expectError: true, + }, + { + name: "valid boundary values", + annotation: "1-65535:443", + port: v1.ServicePort{ + Port: 443, + }, + expectedStart: 1, + expectedEnd: 65535, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start, end, err := portRange(tt.annotation, tt.port) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedStart, start) + assert.Equal(t, tt.expectedEnd, end) + } + }) + } +} diff --git a/pkg/controller/service/nlbv2/loadbalancer_test.go b/pkg/controller/service/nlbv2/loadbalancer_test.go index 5b5fbe52c..502dbd6df 100644 --- a/pkg/controller/service/nlbv2/loadbalancer_test.go +++ b/pkg/controller/service/nlbv2/loadbalancer_test.go @@ -1,8 +1,10 @@ package nlbv2 import ( + "context" "testing" + "github.com/alibabacloud-go/tea/tea" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -10,6 +12,8 @@ import ( "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" ecsmodel "k8s.io/cloud-provider-alibaba-cloud/pkg/model/ecs" + nlbmodel "k8s.io/cloud-provider-alibaba-cloud/pkg/model/nlb" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model/tag" "k8s.io/cloud-provider-alibaba-cloud/pkg/util" ) @@ -497,3 +501,533 @@ func TestDiffAssociatedSecurityGroupPermissions(t *testing.T) { }) } } + +func TestNLBManager_SetProtectionsOff(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr := NewNLBManager(recon.cloud) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + tests := []struct { + name string + remote *nlbmodel.NetworkLoadBalancer + expectError bool + }{ + { + name: "empty load balancer id returns nil", + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "", + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + { + name: "valid load balancer id calls UpdateLoadBalancerProtection", + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mgr.SetProtectionsOff(reqCtx, tt.remote) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestNLBManager_CleanupLoadBalancerTags(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr := NewNLBManager(recon.cloud) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + tests := []struct { + name string + remote *nlbmodel.NetworkLoadBalancer + expectError bool + }{ + { + name: "empty load balancer id returns nil", + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "", + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + { + name: "valid load balancer id with tags", + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + Tags: []tag.Tag{ + { + Key: "key1", + Value: "value1", + }, + }, + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mgr.CleanupLoadBalancerTags(reqCtx, tt.remote) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestUpdateBandwidthPackageId(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr := NewNLBManager(recon.cloud) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + tests := []struct { + name string + local *nlbmodel.NetworkLoadBalancer + remote *nlbmodel.NetworkLoadBalancer + expectError bool + }{ + { + name: "local not set", + local: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: nil, + }, + NamespacedName: util.NamespacedName(svc), + }, + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String("bwp-remote"), + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + { + name: "local equals remote", + local: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String("bwp-1"), + }, + NamespacedName: util.NamespacedName(svc), + }, + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String("bwp-1"), + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + { + name: "detach and attach", + local: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String("bwp-2"), + }, + NamespacedName: util.NamespacedName(svc), + }, + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String("bwp-1"), + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + { + name: "attach only", + local: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String("bwp-3"), + }, + NamespacedName: util.NamespacedName(svc), + }, + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String(""), + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + { + name: "detach only", + local: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String(""), + }, + NamespacedName: util.NamespacedName(svc), + }, + remote: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + BandwidthPackageId: tea.String("bwp-4"), + }, + NamespacedName: util.NamespacedName(svc), + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := updateBandwidthPackageId(mgr, reqCtx, tt.local, tt.remote) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestNLBManager_Update(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr := NewNLBManager(recon.cloud) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + t.Run("address ip version changed", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + AddressIpVersion: "Ipv4", + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + AddressIpVersion: "Ipv6", + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.Update(reqCtx, local, remote) + assert.Error(t, err) + }) + + t.Run("resource group id changed", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + ResourceGroupId: "rg-1", + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + ResourceGroupId: "rg-2", + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.Update(reqCtx, local, remote) + assert.Error(t, err) + }) + + t.Run("address type changed", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + AddressType: "Internet", + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + AddressType: "Intranet", + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.Update(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("zone mappings changed", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + ZoneMappings: []nlbmodel.ZoneMapping{ + { + ZoneId: "cn-hangzhou-a", + VSwitchId: "vsw-1", + }, + }, + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + ZoneMappings: []nlbmodel.ZoneMapping{ + { + ZoneId: "cn-hangzhou-b", + VSwitchId: "vsw-2", + }, + }, + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.Update(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("security group ids changed", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + SecurityGroupIds: []string{"sg-1", "sg-2"}, + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + SecurityGroupIds: []string{"sg-1"}, + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.Update(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("ipv6 address type changed", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + IPv6AddressType: "Internet", + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + IPv6AddressType: "Intranet", + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.Update(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("name changed", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + Name: "new-name", + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + Name: "old-name", + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.Update(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("no changes", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + Name: "test-name", + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-test-id", + Name: "test-name", + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.Update(reqCtx, local, remote) + assert.NoError(t, err) + }) +} + +func TestGetSecurityGroupName(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "demo", + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + assert.Equal(t, "k8s-nlb-"+reqCtx.Anno.GetDefaultLoadBalancerName(), getSecurityGroupName(reqCtx)) +} + +func TestAssociatedSecurityGroupOperations(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + mgr := NewNLBManager(recon.cloud) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "sg-op-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + t.Run("find associated security group", func(t *testing.T) { + _, err := mgr.FindAssociatedSecurityGroup(reqCtx) + assert.Error(t, err) + }) + + t.Run("create associated security group", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + SourceRanges: []string{"192.168.1.0/24"}, + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.CreateAssociatedSecurityGroup(reqCtx, local) + assert.NoError(t, err) + }) + + t.Run("update associated security group early return", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + SourceRangesSecurityGroupId: "", + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{}, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.UpdateAssociatedSecurityGroup(reqCtx, local, remote) + assert.NoError(t, err) + }) + + t.Run("delete associated security group early return", func(t *testing.T) { + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{}, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.DeleteAssociatedSecurityGroup(reqCtx, remote) + assert.NoError(t, err) + }) + + t.Run("update associated security group rules", func(t *testing.T) { + local := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + SourceRangesSecurityGroupId: "sg-test", + SourceRanges: []string{"192.168.1.0/24"}, + }, + NamespacedName: util.NamespacedName(svc), + } + remote := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + SourceRangesSecurityGroupId: "sg-test", + }, + AssociatedSecurityGroup: &ecsmodel.SecurityGroup{ + ID: "sg-test", + Permissions: []ecsmodel.SecurityGroupPermission{}, + }, + NamespacedName: util.NamespacedName(svc), + } + err := mgr.updateAssociatedSecurityGroupRules(reqCtx, local, remote) + assert.NoError(t, err) + }) +} diff --git a/pkg/controller/service/nlbv2/model_applier_test.go b/pkg/controller/service/nlbv2/model_applier_test.go index eccab4fb6..fcf102db6 100644 --- a/pkg/controller/service/nlbv2/model_applier_test.go +++ b/pkg/controller/service/nlbv2/model_applier_test.go @@ -2,15 +2,20 @@ package nlbv2 import ( "context" + "testing" + "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" utilfeature "k8s.io/apiserver/pkg/util/feature" ctrlCfg "k8s.io/cloud-provider-alibaba-cloud/pkg/config" + "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" + nlbmodel "k8s.io/cloud-provider-alibaba-cloud/pkg/model/nlb" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model/tag" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" - "testing" + "k8s.io/cloud-provider-alibaba-cloud/pkg/util" ) func TestModelApplier_Apply_CreateNLB(t *testing.T) { @@ -268,3 +273,151 @@ func TestModelApplier_Apply_VServerGroup(t *testing.T) { _, err = recon.applier.Apply(reqCtx, localModel) assert.Equal(t, nil, err) } + +func TestIsNLBReusable(t *testing.T) { + tests := []struct { + name string + service *v1.Service + tags []tag.Tag + dnsName string + expected bool + reason string + }{ + { + name: "reusable with no tags", + service: &v1.Service{ + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{}, + }, + }, + tags: []tag.Tag{}, + dnsName: "test-dns", + expected: true, + reason: "", + }, + { + name: "not reusable with TAGKEY tag", + service: &v1.Service{ + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{}, + }, + }, + tags: []tag.Tag{ + { + Key: helper.TAGKEY, + Value: "cluster-id", + }, + }, + dnsName: "test-dns", + expected: false, + reason: "can not reuse loadbalancer created by kubernetes.", + }, + { + name: "not reusable with ClusterTagKey tag", + service: &v1.Service{ + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{}, + }, + }, + tags: []tag.Tag{ + { + Key: util.ClusterTagKey, + Value: "cluster-id", + }, + }, + dnsName: "test-dns", + expected: false, + reason: "can not reuse loadbalancer created by kubernetes.", + }, + { + name: "reusable with matching dnsName", + service: &v1.Service{ + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + { + Hostname: "test-dns", + }, + }, + }, + }, + }, + tags: []tag.Tag{}, + dnsName: "test-dns", + expected: true, + reason: "", + }, + { + name: "not reusable with different dnsName", + service: &v1.Service{ + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + { + Hostname: "other-dns", + }, + }, + }, + }, + }, + tags: []tag.Tag{}, + dnsName: "test-dns", + expected: false, + reason: "service has been associated with dnsname", + }, + { + name: "reusable with multiple ingress matching dnsName", + service: &v1.Service{ + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + { + Hostname: "other-dns", + }, + { + Hostname: "test-dns", + }, + }, + }, + }, + }, + tags: []tag.Tag{}, + dnsName: "test-dns", + expected: true, + reason: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reusable, reason := isNLBReusable(tt.service, tt.tags, tt.dnsName) + assert.Equal(t, tt.expected, reusable) + if tt.reason != "" { + assert.Contains(t, reason, tt.reason) + } + }) + } +} + +func TestIsListenerPortMatch(t *testing.T) { + t.Run("ListenerPort match", func(t *testing.T) { + l := &nlbmodel.ListenerAttribute{ListenerPort: 80} + r := &nlbmodel.ListenerAttribute{ListenerPort: 80} + assert.True(t, isListenerPortMatch(l, r)) + }) + t.Run("ListenerPort mismatch", func(t *testing.T) { + l := &nlbmodel.ListenerAttribute{ListenerPort: 80} + r := &nlbmodel.ListenerAttribute{ListenerPort: 443} + assert.False(t, isListenerPortMatch(l, r)) + }) + t.Run("StartPort EndPort match", func(t *testing.T) { + l := &nlbmodel.ListenerAttribute{StartPort: 1000, EndPort: 2000} + r := &nlbmodel.ListenerAttribute{StartPort: 1000, EndPort: 2000} + assert.True(t, isListenerPortMatch(l, r)) + }) + t.Run("StartPort EndPort mismatch", func(t *testing.T) { + l := &nlbmodel.ListenerAttribute{StartPort: 1000, EndPort: 2000} + r := &nlbmodel.ListenerAttribute{StartPort: 1000, EndPort: 3000} + assert.False(t, isListenerPortMatch(l, r)) + }) +} diff --git a/pkg/controller/service/nlbv2/nlb_controller_test.go b/pkg/controller/service/nlbv2/nlb_controller_test.go index 8394b6bc4..b18905bbd 100644 --- a/pkg/controller/service/nlbv2/nlb_controller_test.go +++ b/pkg/controller/service/nlbv2/nlb_controller_test.go @@ -17,6 +17,8 @@ import ( "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" + ecsmodel "k8s.io/cloud-provider-alibaba-cloud/pkg/model/ecs" + nlbmodel "k8s.io/cloud-provider-alibaba-cloud/pkg/model/nlb" prvd "k8s.io/cloud-provider-alibaba-cloud/pkg/provider" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/vmock" "k8s.io/cloud-provider-alibaba-cloud/pkg/util" @@ -342,3 +344,376 @@ func TestRateLimiter(t *testing.T) { t.Logf("ep duration %f", d.Seconds()) } } + +func TestUpdateReadinessCondition(t *testing.T) { + t.Run("successful update", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := getReqCtx(svc) + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-1", + Namespace: v1.NamespaceDefault, + }, + } + err = recon.kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + sgs := []*nlbmodel.ServerGroup{ + { + InitialServers: []nlbmodel.ServerGroupServer{ + { + TargetRef: &v1.ObjectReference{ + Namespace: v1.NamespaceDefault, + Name: "test-pod-1", + }, + }, + }, + }, + } + + err = recon.updateReadinessCondition(reqCtx, sgs) + assert.NoError(t, err) + }) + + t.Run("TargetRef is nil", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := getReqCtx(svc) + + sgs := []*nlbmodel.ServerGroup{ + { + InitialServers: []nlbmodel.ServerGroupServer{ + { + TargetRef: nil, + }, + }, + }, + } + + err = recon.updateReadinessCondition(reqCtx, sgs) + assert.NoError(t, err) + }) + + t.Run("duplicate pod", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := getReqCtx(svc) + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-2", + Namespace: v1.NamespaceDefault, + }, + } + err = recon.kubeClient.Create(context.TODO(), pod) + assert.NoError(t, err) + + sgs := []*nlbmodel.ServerGroup{ + { + InitialServers: []nlbmodel.ServerGroupServer{ + { + TargetRef: &v1.ObjectReference{ + Namespace: v1.NamespaceDefault, + Name: "test-pod-2", + }, + }, + { + TargetRef: &v1.ObjectReference{ + Namespace: v1.NamespaceDefault, + Name: "test-pod-2", + }, + }, + }, + }, + } + + err = recon.updateReadinessCondition(reqCtx, sgs) + assert.NoError(t, err) + }) + + t.Run("pod not found", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := getReqCtx(svc) + + sgs := []*nlbmodel.ServerGroup{ + { + InitialServers: []nlbmodel.ServerGroupServer{ + { + TargetRef: &v1.ObjectReference{ + Namespace: v1.NamespaceDefault, + Name: "non-existent-pod", + }, + }, + }, + }, + } + + err = recon.updateReadinessCondition(reqCtx, sgs) + assert.NoError(t, err) + }) + + t.Run("multiple server groups", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := getReqCtx(svc) + + pod1 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-3", + Namespace: v1.NamespaceDefault, + }, + } + pod2 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-4", + Namespace: v1.NamespaceDefault, + }, + } + err = recon.kubeClient.Create(context.TODO(), pod1) + assert.NoError(t, err) + err = recon.kubeClient.Create(context.TODO(), pod2) + assert.NoError(t, err) + + sgs := []*nlbmodel.ServerGroup{ + { + InitialServers: []nlbmodel.ServerGroupServer{ + { + TargetRef: &v1.ObjectReference{ + Namespace: v1.NamespaceDefault, + Name: "test-pod-3", + }, + }, + }, + }, + { + InitialServers: []nlbmodel.ServerGroupServer{ + { + TargetRef: &v1.ObjectReference{ + Namespace: v1.NamespaceDefault, + Name: "test-pod-4", + }, + }, + }, + }, + } + + err = recon.updateReadinessCondition(reqCtx, sgs) + assert.NoError(t, err) + }) + + t.Run("empty server groups", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: ServiceName, + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := getReqCtx(svc) + + sgs := []*nlbmodel.ServerGroup{} + + err = recon.updateReadinessCondition(reqCtx, sgs) + assert.NoError(t, err) + }) +} + +func TestNLBControllerFunctions(t *testing.T) { + t.Run("newReconciler function exists and can be called", func(t *testing.T) { + // Simply test that the function exists and signature is correct + // We can't fully test it without a real manager and context + assert.NotNil(t, newReconciler) + }) +} + +func TestReconcileNLB_UpdateServiceLabels(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-label-test", + Namespace: v1.NamespaceDefault, + Labels: map[string]string{}, + }, + } + + cl := fake.NewClientBuilder(). + WithStatusSubresource(&v1.Service{}). + WithRuntimeObjects(svc). + Build() + recon := &ReconcileNLB{kubeClient: cl} + + lb := &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + LoadBalancerId: "nlb-123", + }, + AssociatedSecurityGroup: &ecsmodel.SecurityGroup{ + ID: "sg-123", + }, + } + + err := recon.updateServiceLabels(svc, lb) + assert.NoError(t, err) + + updated := &v1.Service{} + err = cl.Get(context.TODO(), types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}, updated) + assert.NoError(t, err) + assert.Equal(t, "nlb-123", updated.Labels[helper.LabelLoadBalancerId]) + assert.Equal(t, "sg-123", updated.Labels[helper.LabelSecurityGroupId]) + assert.NotEmpty(t, updated.Labels[helper.LabelServiceHash]) +} + +func TestReconcileNLB_RemoveServiceLabels(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-remove-label-test", + Namespace: v1.NamespaceDefault, + Labels: map[string]string{ + helper.LabelServiceHash: "hash", + helper.LabelLoadBalancerId: "nlb-1", + helper.LabelSecurityGroupId: "sg-1", + }, + }, + } + + cl := fake.NewClientBuilder(). + WithStatusSubresource(&v1.Service{}). + WithRuntimeObjects(svc). + Build() + recon := &ReconcileNLB{kubeClient: cl} + + err := recon.removeServiceLabels(svc) + assert.NoError(t, err) + + updated := &v1.Service{} + err = cl.Get(context.TODO(), types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}, updated) + assert.NoError(t, err) + assert.NotNil(t, updated.Labels) +} + +func TestReconcileNLB_ReconcileLoadBalancerResources(t *testing.T) { + t.Run("success path", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + svc := &v1.Service{} + err = recon.kubeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, svc) + assert.NoError(t, err) + + err = recon.reconcileLoadBalancerResources(getReqCtx(svc)) + assert.NoError(t, err) + }) + + t.Run("build and apply model failed", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + badSvc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bad-zone-map", + Namespace: v1.NamespaceDefault, + Annotations: map[string]string{ + annotation.Annotation(annotation.ZoneMaps): "invalid", + }, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeLoadBalancer, + LoadBalancerClass: tea.String(helper.NLBClass), + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(80), + Protocol: v1.ProtocolTCP, + }, + }, + }, + } + + err = recon.kubeClient.Create(context.TODO(), badSvc) + assert.NoError(t, err) + + err = recon.reconcileLoadBalancerResources(getReqCtx(badSvc)) + assert.Error(t, err) + }) +} + +func TestReconcileNLB_UpdateAndRemoveServiceStatus(t *testing.T) { + t.Run("update service status lb nil", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "status-nil-lb", + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := getReqCtx(svc) + + err = recon.updateServiceStatus(reqCtx, svc, nil) + assert.Error(t, err) + }) + + t.Run("remove service status no change", func(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "status-no-change", + Namespace: v1.NamespaceDefault, + }, + } + reqCtx := getReqCtx(svc) + + err = recon.removeServiceStatus(reqCtx, svc) + assert.NoError(t, err) + }) +} + +func TestHasInvalidServerInServerGroups(t *testing.T) { + assert.False(t, hasInvalidServerInServerGroups(nil)) + assert.False(t, hasInvalidServerInServerGroups([]*nlbmodel.ServerGroup{ + nil, + {InvalidServers: nil}, + })) + assert.True(t, hasInvalidServerInServerGroups([]*nlbmodel.ServerGroup{ + {InvalidServers: []nlbmodel.ServerGroupServer{{ServerId: "s-1"}}}, + })) +} diff --git a/pkg/controller/service/nlbv2/server_groups_test.go b/pkg/controller/service/nlbv2/server_groups_test.go index 3ae61ab1f..a909dbfb9 100644 --- a/pkg/controller/service/nlbv2/server_groups_test.go +++ b/pkg/controller/service/nlbv2/server_groups_test.go @@ -1,14 +1,26 @@ package nlbv2 import ( + "context" "testing" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/tools/record" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" + reconbackend "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/backend" + svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model" nlbmodel "k8s.io/cloud-provider-alibaba-cloud/pkg/model/nlb" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model/tag" + "k8s.io/cloud-provider-alibaba-cloud/pkg/util" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func makeNLBBackends(n int) []nlbmodel.ServerGroupServer { @@ -151,3 +163,3978 @@ func TestSetServerGroupAttributeFromAnno_InvalidDefaultWeight_Negative(t *testin assert.Error(t, err) assert.Contains(t, err.Error(), "default weight must be integer in range [0,100]") } + +func TestIsServerEqual(t *testing.T) { + tests := []struct { + name string + a nlbmodel.ServerGroupServer + b nlbmodel.ServerGroupServer + expected bool + }{ + { + name: "different server types", + a: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + }, + b: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EniServerType, + ServerId: "ecs-1", + }, + expected: false, + }, + { + name: "ecs server type - same id", + a: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + }, + b: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + }, + expected: true, + }, + { + name: "ecs server type - different id", + a: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + }, + b: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-2", + }, + expected: false, + }, + { + name: "eni server type - same id and ip", + a: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EniServerType, + ServerId: "eni-1", + ServerIp: "10.0.0.1", + }, + b: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EniServerType, + ServerId: "eni-1", + ServerIp: "10.0.0.1", + }, + expected: true, + }, + { + name: "eni server type - different ip", + a: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EniServerType, + ServerId: "eni-1", + ServerIp: "10.0.0.1", + }, + b: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.EniServerType, + ServerId: "eni-1", + ServerIp: "10.0.0.2", + }, + expected: false, + }, + { + name: "ip server type - same id and ip", + a: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.IpServerType, + ServerId: "ip-1", + ServerIp: "10.0.0.1", + }, + b: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.IpServerType, + ServerId: "ip-1", + ServerIp: "10.0.0.1", + }, + expected: true, + }, + { + name: "ip server type - different id", + a: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.IpServerType, + ServerId: "ip-1", + ServerIp: "10.0.0.1", + }, + b: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.IpServerType, + ServerId: "ip-2", + ServerIp: "10.0.0.1", + }, + expected: false, + }, + { + name: "unsupported server type", + a: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.ServerType("Unknown"), + ServerId: "id-1", + }, + b: nlbmodel.ServerGroupServer{ + ServerType: nlbmodel.ServerType("Unknown"), + ServerId: "id-1", + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isServerEqual(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestDiff(t *testing.T) { + tests := []struct { + name string + remote *nlbmodel.ServerGroup + local *nlbmodel.ServerGroup + expectedAdds int + expectedDels int + expectedUpdates int + }{ + { + name: "empty remote and local", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{}, + }, + local: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectedAdds: 0, + expectedDels: 0, + expectedUpdates: 0, + }, + { + name: "add new server", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{}, + }, + local: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + expectedAdds: 1, + expectedDels: 0, + expectedUpdates: 0, + }, + { + name: "delete server", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + local: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectedAdds: 0, + expectedDels: 1, + expectedUpdates: 0, + }, + { + name: "update server weight", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + local: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 200, + }, + }, + }, + expectedAdds: 0, + expectedDels: 0, + expectedUpdates: 1, + }, + { + name: "update server port", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + local: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 8080, + Weight: 100, + }, + }, + }, + expectedAdds: 0, + expectedDels: 0, + expectedUpdates: 1, + }, + { + name: "update server description", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + Description: "old", + }, + }, + }, + local: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + Description: "new", + }, + }, + }, + expectedAdds: 0, + expectedDels: 0, + expectedUpdates: 1, + }, + { + name: "ignore user managed servers", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + IsUserManaged: true, + Port: 80, + Weight: 100, + }, + }, + }, + local: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectedAdds: 0, + expectedDels: 0, + expectedUpdates: 0, + }, + { + name: "ignore weight update when IgnoreWeightUpdate is true", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + local: &nlbmodel.ServerGroup{ + IgnoreWeightUpdate: true, + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 200, + }, + }, + }, + expectedAdds: 0, + expectedDels: 0, + expectedUpdates: 0, + }, + { + name: "complex scenario", + remote: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-2", + Port: 80, + Weight: 100, + }, + }, + }, + local: &nlbmodel.ServerGroup{ + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 200, + }, + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-3", + Port: 80, + Weight: 100, + }, + }, + }, + expectedAdds: 1, + expectedDels: 1, + expectedUpdates: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adds, dels, updates := diff(tt.remote, tt.local) + assert.Equal(t, tt.expectedAdds, len(adds)) + assert.Equal(t, tt.expectedDels, len(dels)) + assert.Equal(t, tt.expectedUpdates, len(updates)) + }) + } +} + +func TestPodPercentAlgorithm(t *testing.T) { + tests := []struct { + name string + mode helper.TrafficPolicy + backends []nlbmodel.ServerGroupServer + weight int + validate func(t *testing.T, result []nlbmodel.ServerGroupServer) + }{ + { + name: "empty backends", + mode: helper.ClusterTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{}, + weight: 100, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 0, len(result)) + }, + }, + { + name: "zero weight", + mode: helper.ClusterTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{ + {ServerId: "ecs-1"}, + {ServerId: "ecs-2"}, + }, + weight: 0, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, int32(0), result[0].Weight) + assert.Equal(t, int32(0), result[1].Weight) + }, + }, + { + name: "cluster traffic policy - equal distribution", + mode: helper.ClusterTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{ + {ServerId: "ecs-1"}, + {ServerId: "ecs-2"}, + }, + weight: 100, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, int32(50), result[0].Weight) + assert.Equal(t, int32(50), result[1].Weight) + }, + }, + { + name: "eni traffic policy - equal distribution", + mode: helper.ENITrafficPolicy, + backends: []nlbmodel.ServerGroupServer{ + {ServerId: "ecs-1"}, + {ServerId: "ecs-2"}, + }, + weight: 100, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, int32(50), result[0].Weight) + assert.Equal(t, int32(50), result[1].Weight) + }, + }, + { + name: "cluster traffic policy - weight less than backend count", + mode: helper.ClusterTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{ + {ServerId: "ecs-1"}, + {ServerId: "ecs-2"}, + {ServerId: "ecs-3"}, + }, + weight: 2, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 3, len(result)) + assert.Equal(t, int32(1), result[0].Weight) + assert.Equal(t, int32(1), result[1].Weight) + assert.Equal(t, int32(1), result[2].Weight) + }, + }, + { + name: "local traffic policy - same pods per ecs", + mode: helper.LocalTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{ + {ServerId: "ecs-1"}, + {ServerId: "ecs-1"}, + {ServerId: "ecs-2"}, + {ServerId: "ecs-2"}, + }, + weight: 100, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 4, len(result)) + ecs1Weight := result[0].Weight + ecs2Weight := result[2].Weight + assert.Equal(t, ecs1Weight, result[1].Weight) + assert.Equal(t, ecs2Weight, result[3].Weight) + assert.True(t, ecs1Weight >= 1) + assert.True(t, ecs2Weight >= 1) + }, + }, + { + name: "local traffic policy - different pods per ecs", + mode: helper.LocalTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{ + {ServerId: "ecs-1", ServerType: nlbmodel.EcsServerType}, + {ServerId: "ecs-1", ServerType: nlbmodel.EcsServerType}, + {ServerId: "ecs-1", ServerType: nlbmodel.EcsServerType}, + {ServerId: "ecs-2", ServerType: nlbmodel.EcsServerType}, + }, + weight: 100, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 4, len(result)) + ecs1Weight := result[0].Weight + ecs2Weight := result[3].Weight + assert.Equal(t, ecs1Weight, result[1].Weight) + assert.Equal(t, ecs1Weight, result[2].Weight) + assert.True(t, ecs1Weight > ecs2Weight) + assert.True(t, ecs1Weight >= 1) + assert.True(t, ecs2Weight >= 1) + }, + }, + { + name: "local traffic policy - minimum weight is 1", + mode: helper.LocalTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{ + {ServerId: "ecs-1"}, + {ServerId: "ecs-2"}, + {ServerId: "ecs-3"}, + {ServerId: "ecs-4"}, + {ServerId: "ecs-5"}, + }, + weight: 2, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 5, len(result)) + for _, backend := range result { + assert.True(t, backend.Weight >= 1) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := podPercentAlgorithm(tt.mode, tt.backends, tt.weight) + tt.validate(t, result) + }) + } +} + +func TestGetAnyPortServerGroupNamedKey(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-svc", + }, + } + + tests := []struct { + name string + protocol string + startPort int32 + endPort int32 + validate func(t *testing.T, key *nlbmodel.SGNamedKey) + }{ + { + name: "basic test", + protocol: "TCP", + startPort: 80, + endPort: 90, + validate: func(t *testing.T, key *nlbmodel.SGNamedKey) { + assert.NotNil(t, key) + assert.Equal(t, "default", key.Namespace) + assert.Equal(t, "test-svc", key.ServiceName) + assert.Equal(t, "TCP", key.Protocol) + assert.Equal(t, "80_90", key.SGGroupPort) + }, + }, + { + name: "UDP protocol", + protocol: "UDP", + startPort: 53, + endPort: 53, + validate: func(t *testing.T, key *nlbmodel.SGNamedKey) { + assert.NotNil(t, key) + assert.Equal(t, "UDP", key.Protocol) + assert.Equal(t, "53_53", key.SGGroupPort) + }, + }, + { + name: "TCPSSL protocol", + protocol: "TCPSSL", + startPort: 443, + endPort: 443, + validate: func(t *testing.T, key *nlbmodel.SGNamedKey) { + assert.NotNil(t, key) + assert.Equal(t, "TCPSSL", key.Protocol) + assert.Equal(t, "443_443", key.SGGroupPort) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key := getAnyPortServerGroupNamedKey(svc, tt.protocol, tt.startPort, tt.endPort) + tt.validate(t, key) + }) + } +} + +func TestServerGroupManager_BatchRemoveServers(t *testing.T) { + recon, err := getReconcileNLB() + assert.Equal(t, nil, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.Equal(t, nil, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + sg := &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + } + + delServers := []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + }, + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-2", + Port: 80, + }, + } + + err = mgr.BatchRemoveServers(reqCtx, sg, delServers) + assert.Equal(t, nil, err) +} + +func TestServerGroupManager_BatchUpdateServers(t *testing.T) { + recon, err := getReconcileNLB() + assert.Equal(t, nil, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.Equal(t, nil, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + sg := &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + } + + updateServers := []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-2", + Port: 80, + Weight: 200, + }, + } + + err = mgr.BatchUpdateServers(reqCtx, sg, updateServers) + assert.Equal(t, nil, err) +} + +func TestGetServerGroupIDs(t *testing.T) { + tests := []struct { + name string + annotation string + expected []string + expectErr bool + }{ + { + name: "empty annotation", + annotation: "", + expected: nil, + expectErr: false, + }, + { + name: "single server group", + annotation: "sgp-xxx:443", + expected: []string{"sgp-xxx"}, + expectErr: false, + }, + { + name: "multiple server groups", + annotation: "sgp-xxx:443,sgp-yyy:80", + expected: []string{"sgp-xxx", "sgp-yyy"}, + expectErr: false, + }, + { + name: "invalid format - no colon", + annotation: "sgp-xxx", + expected: nil, + expectErr: true, + }, + { + name: "invalid format - empty after colon", + annotation: "sgp-xxx:", + expected: []string{"sgp-xxx"}, + expectErr: false, + }, + { + name: "multiple colons", + annotation: "sgp-xxx:443:extra", + expected: []string{"sgp-xxx"}, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := getServerGroupIDs(tt.annotation) + if tt.expectErr { + assert.NotNil(t, err) + } else { + assert.Equal(t, nil, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestServerGroupManager_DeleteServerGroup(t *testing.T) { + recon, err := getReconcileNLB() + assert.Equal(t, nil, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.Equal(t, nil, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + err = mgr.DeleteServerGroup(reqCtx, "sg-test-id") + assert.Equal(t, nil, err) +} + +func TestServerGroupManager_UpdateServerGroup(t *testing.T) { + recon, err := getReconcileNLB() + assert.Equal(t, nil, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.Equal(t, nil, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + local := &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + Scheduler: "Wrr", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + } + + remote := &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + Scheduler: "Wlc", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 50, + }, + }, + } + + err = mgr.UpdateServerGroup(reqCtx, local, remote) + assert.Equal(t, nil, err) +} + +func TestServerGroupManager_UpdateServerGroup_UserManaged(t *testing.T) { + recon, err := getReconcileNLB() + assert.Equal(t, nil, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.Equal(t, nil, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + local := &nlbmodel.ServerGroup{ + IsUserManaged: true, + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + Servers: []nlbmodel.ServerGroupServer{}, + } + + remote := &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + Servers: []nlbmodel.ServerGroupServer{}, + } + + err = mgr.UpdateServerGroup(reqCtx, local, remote) + assert.Equal(t, nil, err) +} + +func TestServerGroupManager_UpdateServerGroup_HealthCheck(t *testing.T) { + recon, err := getReconcileNLB() + assert.Equal(t, nil, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.Equal(t, nil, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + enabled := true + local := &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckType: "TCP", + HealthyThreshold: 3, + }, + Servers: []nlbmodel.ServerGroupServer{}, + } + + remote := &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckType: "HTTP", + HealthyThreshold: 5, + }, + Servers: []nlbmodel.ServerGroupServer{}, + } + + err = mgr.UpdateServerGroup(reqCtx, local, remote) + assert.Equal(t, nil, err) +} + +func TestServerGroupManager_UpdateServerGroup_ConnectionDrain(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + enabled := true + disabled := false + tests := []struct { + name string + local *nlbmodel.ServerGroup + remote *nlbmodel.ServerGroup + expectError bool + }{ + { + name: "update ConnectionDrainEnabled", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + ConnectionDrainEnabled: &enabled, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + ConnectionDrainEnabled: &disabled, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update ConnectionDrainTimeout", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + ConnectionDrainTimeout: 30, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + ConnectionDrainTimeout: 60, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update PreserveClientIpEnabled", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + PreserveClientIpEnabled: &enabled, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + PreserveClientIpEnabled: &disabled, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mgr.UpdateServerGroup(reqCtx, tt.local, tt.remote) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestServerGroupManager_UpdateServerGroup_HealthCheckDetails(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + enabled := true + tests := []struct { + name string + local *nlbmodel.ServerGroup + remote *nlbmodel.ServerGroup + expectError bool + }{ + { + name: "update HealthCheckConnectPort", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckConnectPort: 8080, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckConnectPort: 80, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update UnhealthyThreshold", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + UnhealthyThreshold: 3, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + UnhealthyThreshold: 5, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update HealthCheckConnectTimeout", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckConnectTimeout: 5, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckConnectTimeout: 10, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update HealthCheckInterval", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckInterval: 5, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckInterval: 10, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update HealthCheckDomain", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckDomain: "example.com", + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckDomain: "test.com", + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update HealthCheckUrl", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckUrl: "/health", + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckUrl: "/check", + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update HttpCheckMethod", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HttpCheckMethod: "GET", + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HttpCheckMethod: "POST", + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "update HealthCheckHttpCode", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckHttpCode: []string{"200", "201"}, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckHttpCode: []string{"200"}, + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + { + name: "add HealthCheckConfig when remote is nil", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-local-id", + ServerGroupName: "local-sg", + HealthCheckConfig: &nlbmodel.HealthCheckConfig{ + HealthCheckEnabled: &enabled, + HealthCheckType: "TCP", + }, + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-remote-id", + ServerGroupName: "remote-sg", + HealthCheckConfig: nil, + Servers: []nlbmodel.ServerGroupServer{}, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mgr.UpdateServerGroup(reqCtx, tt.local, tt.remote) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestServerGroupManager_CreateServerGroup(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + tests := []struct { + name string + serverGroup *nlbmodel.ServerGroup + expectError bool + validate func(t *testing.T, sg *nlbmodel.ServerGroup) + }{ + { + name: "create server group successfully", + serverGroup: &nlbmodel.ServerGroup{ + ServerGroupName: "test-sg", + Protocol: nlbmodel.TCP, + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.NotEmpty(t, sg.ServerGroupId) + }, + }, + { + name: "create server group with empty ResourceGroupId", + serverGroup: &nlbmodel.ServerGroup{ + ServerGroupName: "test-sg", + Protocol: nlbmodel.TCP, + ResourceGroupId: "", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.NotEmpty(t, sg.ServerGroupId) + }, + }, + { + name: "create server group with existing ResourceGroupId", + serverGroup: &nlbmodel.ServerGroup{ + ServerGroupName: "test-sg", + Protocol: nlbmodel.TCP, + ResourceGroupId: "rg-test-id", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.Equal(t, "rg-test-id", sg.ResourceGroupId) + assert.NotEmpty(t, sg.ServerGroupId) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sgCopy := *tt.serverGroup + err := mgr.CreateServerGroup(reqCtx, &sgCopy) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, &sgCopy) + } + } + }) + } +} + +func TestServerGroupManager_CleanupServerGroupTags(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + tests := []struct { + name string + serverGroup *nlbmodel.ServerGroup + expectError bool + }{ + { + name: "empty tags returns nil", + serverGroup: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Tags: []tag.Tag{}, + }, + expectError: false, + }, + { + name: "tags with default tags", + serverGroup: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Tags: []tag.Tag{ + { + Key: "key1", + Value: "value1", + }, + }, + }, + expectError: false, + }, + { + name: "tags matching default tags should be deleted", + serverGroup: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Tags: []tag.Tag{ + { + Key: "ack.aliyun.com", + Value: "a5e4dbfc9c2ae4642b0335607860aef6", + }, + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mgr.CleanupServerGroupTags(reqCtx, tt.serverGroup) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetBackendPort(t *testing.T) { + tests := []struct { + name string + port int32 + anyPort bool + expected int32 + }{ + { + name: "anyPort enabled returns 0", + port: 80, + anyPort: true, + expected: 0, + }, + { + name: "anyPort disabled returns port", + port: 80, + anyPort: false, + expected: 80, + }, + { + name: "anyPort disabled with different port", + port: 443, + anyPort: false, + expected: 443, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getBackendPort(tt.port, tt.anyPort) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsServerManagedByMyService(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "test-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + tests := []struct { + name string + server nlbmodel.ServerGroupServer + expected bool + }{ + { + name: "server managed by my service", + server: nlbmodel.ServerGroupServer{ + Description: "k8s.80.TCP.test-svc.default.clusterid", + }, + expected: true, + }, + { + name: "server managed by different service", + server: nlbmodel.ServerGroupServer{ + Description: "k8s.80.TCP.other-svc.default.clusterid", + }, + expected: false, + }, + { + name: "server managed by different namespace", + server: nlbmodel.ServerGroupServer{ + Description: "k8s.80.TCP.test-svc.other-ns.clusterid", + }, + expected: false, + }, + { + name: "invalid description format", + server: nlbmodel.ServerGroupServer{ + Description: "invalid-description", + }, + expected: false, + }, + { + name: "empty description", + server: nlbmodel.ServerGroupServer{ + Description: "", + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isServerManagedByMyService(reqCtx, tt.server) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestSetWeightBackends(t *testing.T) { + tests := []struct { + name string + mode helper.TrafficPolicy + backends []nlbmodel.ServerGroupServer + weight *int + validate func(t *testing.T, result []nlbmodel.ServerGroupServer) + }{ + { + name: "nil weight uses default algorithm", + mode: helper.ClusterTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{{ServerId: "ecs-1"}, {ServerId: "ecs-2"}}, + weight: nil, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, int32(100), result[0].Weight) + assert.Equal(t, int32(100), result[1].Weight) + }, + }, + { + name: "weight 50 with cluster mode", + mode: helper.ClusterTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{{ServerId: "ecs-1"}, {ServerId: "ecs-2"}}, + weight: intPtr(50), + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, int32(25), result[0].Weight) + assert.Equal(t, int32(25), result[1].Weight) + }, + }, + { + name: "weight 0 sets all weights to 0", + mode: helper.ENITrafficPolicy, + backends: []nlbmodel.ServerGroupServer{{ServerId: "ecs-1"}, {ServerId: "ecs-2"}}, + weight: intPtr(0), + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, int32(0), result[0].Weight) + assert.Equal(t, int32(0), result[1].Weight) + }, + }, + { + name: "weight with local mode", + mode: helper.LocalTrafficPolicy, + backends: []nlbmodel.ServerGroupServer{{ServerId: "ecs-1"}, {ServerId: "ecs-1"}, {ServerId: "ecs-2"}}, + weight: intPtr(100), + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 3, len(result)) + assert.True(t, result[0].Weight >= 1) + assert.True(t, result[1].Weight >= 1) + assert.True(t, result[2].Weight >= 1) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := setWeightBackends(tt.mode, tt.backends, tt.weight, pointer.Int(DefaultServerWeight)) + tt.validate(t, result) + }) + } +} + +func intPtr(i int) *int { + return &i +} + +func TestUpdateENIBackends(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + tests := []struct { + name string + backends []nlbmodel.ServerGroupServer + ipVersion model.AddressIPVersionType + serverGroupType nlbmodel.ServerGroupType + validate func(t *testing.T, result []nlbmodel.ServerGroupServer) + }{ + { + name: "IpServerGroupType sets ServerId and ServerType", + backends: []nlbmodel.ServerGroupServer{ + {ServerIp: "10.0.0.1"}, + {ServerIp: "10.0.0.2"}, + }, + ipVersion: model.IPv4, + serverGroupType: nlbmodel.IpServerGroupType, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, "10.0.0.1", result[0].ServerId) + assert.Equal(t, "10.0.0.1", result[0].ServerIp) + assert.Equal(t, nlbmodel.IpServerType, result[0].ServerType) + assert.Equal(t, "10.0.0.2", result[1].ServerId) + assert.Equal(t, "10.0.0.2", result[1].ServerIp) + assert.Equal(t, nlbmodel.IpServerType, result[1].ServerType) + }, + }, + { + name: "InstanceServerGroupType sets EniServerType", + backends: []nlbmodel.ServerGroupServer{ + {ServerIp: "10.0.0.1"}, + }, + ipVersion: model.IPv4, + serverGroupType: nlbmodel.InstanceServerGroupType, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 1, len(result)) + assert.Equal(t, nlbmodel.EniServerType, result[0].ServerType) + }, + }, + { + name: "empty backends", + backends: []nlbmodel.ServerGroupServer{}, + ipVersion: model.IPv4, + serverGroupType: nlbmodel.InstanceServerGroupType, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 0, len(result)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := updateENIBackends(mgr, tt.backends, tt.ipVersion, tt.serverGroupType) + assert.NoError(t, err) + tt.validate(t, result) + }) + } +} + +func TestSetServerGroupAttributeFromAnno(t *testing.T) { + tests := []struct { + name string + sg *nlbmodel.ServerGroup + anno map[string]string + expectError bool + validate func(t *testing.T, sg *nlbmodel.ServerGroup) + }{ + { + name: "set ServerGroupType to Ip", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.ServerGroupType): "ip", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.Equal(t, nlbmodel.IpServerGroupType, sg.ServerGroupType) + }, + }, + { + name: "set ServerGroupType to Instance", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.ServerGroupType): "instance", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.Equal(t, nlbmodel.InstanceServerGroupType, sg.ServerGroupType) + }, + }, + { + name: "set ConnectionDrain enabled", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.ConnectionDrain): "on", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.NotNil(t, sg.ConnectionDrainEnabled) + assert.True(t, *sg.ConnectionDrainEnabled) + }, + }, + { + name: "set ConnectionDrainTimeout", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.ConnectionDrainTimeout): "30", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.Equal(t, int32(30), sg.ConnectionDrainTimeout) + }, + }, + { + name: "set Scheduler", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.Scheduler): "Wrr", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.Equal(t, "Wrr", sg.Scheduler) + }, + }, + { + name: "set PreserveClientIp enabled", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.PreserveClientIp): "on", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.NotNil(t, sg.PreserveClientIpEnabled) + assert.True(t, *sg.PreserveClientIpEnabled) + }, + }, + { + name: "set ResourceGroupId", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.ResourceGroupId): "rg-123", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.Equal(t, "rg-123", sg.ResourceGroupId) + }, + }, + { + name: "set HealthCheckConfig", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.HealthCheckFlag): "on", + annotation.Annotation(annotation.HealthCheckType): "TCP", + annotation.Annotation(annotation.HealthCheckConnectPort): "8080", + annotation.Annotation(annotation.HealthyThreshold): "3", + annotation.Annotation(annotation.UnhealthyThreshold): "3", + annotation.Annotation(annotation.HealthCheckConnectTimeout): "5", + annotation.Annotation(annotation.HealthCheckInterval): "10", + annotation.Annotation(annotation.HealthCheckDomain): "example.com", + annotation.Annotation(annotation.HealthCheckURI): "/health", + annotation.Annotation(annotation.HealthCheckMethod): "GET", + annotation.Annotation(annotation.HealthCheckHTTPCode): "200,201", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.NotNil(t, sg.HealthCheckConfig) + assert.NotNil(t, sg.HealthCheckConfig.HealthCheckEnabled) + assert.True(t, *sg.HealthCheckConfig.HealthCheckEnabled) + assert.Equal(t, "TCP", sg.HealthCheckConfig.HealthCheckType) + assert.Equal(t, int32(8080), sg.HealthCheckConfig.HealthCheckConnectPort) + assert.Equal(t, int32(3), sg.HealthCheckConfig.HealthyThreshold) + assert.Equal(t, int32(3), sg.HealthCheckConfig.UnhealthyThreshold) + assert.Equal(t, int32(5), sg.HealthCheckConfig.HealthCheckConnectTimeout) + assert.Equal(t, int32(10), sg.HealthCheckConfig.HealthCheckInterval) + assert.Equal(t, "example.com", sg.HealthCheckConfig.HealthCheckDomain) + assert.Equal(t, "/health", sg.HealthCheckConfig.HealthCheckUrl) + assert.Equal(t, "GET", sg.HealthCheckConfig.HttpCheckMethod) + assert.Equal(t, []string{"200", "201"}, sg.HealthCheckConfig.HealthCheckHttpCode) + }, + }, + { + name: "set IgnoreWeightUpdate", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.IgnoreWeightUpdate): "on", + }, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup) { + assert.True(t, sg.IgnoreWeightUpdate) + }, + }, + { + name: "invalid ServerGroupType", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.ServerGroupType): "invalid", + }, + expectError: true, + }, + { + name: "invalid ConnectionDrainTimeout", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.ConnectionDrainTimeout): "invalid", + }, + expectError: true, + }, + { + name: "invalid HealthCheckConnectPort", + sg: &nlbmodel.ServerGroup{}, + anno: map[string]string{ + annotation.Annotation(annotation.HealthCheckFlag): "on", + annotation.Annotation(annotation.HealthCheckConnectPort): "invalid", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: tt.anno, + }, + } + anno := annotation.NewAnnotationRequest(svc) + err := setServerGroupAttributeFromAnno(tt.sg, anno) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, tt.sg) + } + } + }) + } +} + +func TestServerGroupManager_UpdateServerGroupServers(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: ServiceName, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + tests := []struct { + name string + local *nlbmodel.ServerGroup + remote *nlbmodel.ServerGroup + expectError bool + }{ + { + name: "no changes", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + expectError: false, + }, + { + name: "add server", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-2", + Port: 80, + Weight: 100, + }, + }, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + expectError: false, + }, + { + name: "remove server", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Servers: []nlbmodel.ServerGroupServer{}, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + expectError: false, + }, + { + name: "update server weight", + local: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 200, + }, + }, + }, + remote: &nlbmodel.ServerGroup{ + ServerGroupId: "sg-test-id", + Servers: []nlbmodel.ServerGroupServer{ + { + ServerType: nlbmodel.EcsServerType, + ServerId: "ecs-1", + Port: 80, + Weight: 100, + }, + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mgr.updateServerGroupServers(reqCtx, tt.local, tt.remote) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestServerGroupManager_SetBackendsFromEndpoints(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + nodeName := "test-node" + tests := []struct { + name string + endpoints *v1.Endpoints + servicePort *v1.ServicePort + validate func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) + setupKubeClient func(t *testing.T, kubeClient client.Client) client.Client + }{ + { + name: "empty subsets", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{}, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + assert.False(t, containsPotentialReady) + }, + }, + { + name: "with addresses", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "10.0.0.1", + NodeName: &nodeName, + }, + { + IP: "10.0.0.2", + NodeName: &nodeName, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromString("tcp"), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 2, len(backends)) + assert.Equal(t, "10.0.0.1", backends[0].ServerIp) + assert.Equal(t, "10.0.0.2", backends[1].ServerIp) + assert.Equal(t, int32(8080), backends[0].Port) + }, + }, + { + name: "with int targetPort", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "10.0.0.1", + NodeName: &nodeName, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(9090), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 1, len(backends)) + assert.Equal(t, int32(9090), backends[0].Port) + }, + }, + { + name: "anyPort enabled", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "10.0.0.1", + NodeName: &nodeName, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 1, len(backends)) + }, + }, + { + name: "with NotReadyAddresses without TargetRef", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.3", + NodeName: &nodeName, + TargetRef: nil, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + assert.False(t, containsPotentialReady) + }, + }, + { + name: "with NotReadyAddresses with non-Pod TargetRef", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.3", + NodeName: &nodeName, + TargetRef: &v1.ObjectReference{ + Kind: "Node", + Namespace: v1.NamespaceDefault, + Name: "test-pod", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + assert.False(t, containsPotentialReady) + }, + }, + { + name: "with NotReadyAddresses with Pod not found", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.3", + NodeName: &nodeName, + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Namespace: v1.NamespaceDefault, + Name: "not-found-pod", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + assert.True(t, containsPotentialReady) + }, + }, + { + name: "with NotReadyAddresses with Pod without readiness gate", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.3", + NodeName: &nodeName, + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Namespace: v1.NamespaceDefault, + Name: "test-pod-no-gate", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + assert.False(t, containsPotentialReady) + }, + setupKubeClient: func(t *testing.T, kubeClient client.Client) client.Client { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-no-gate", + Namespace: v1.NamespaceDefault, + }, + Spec: v1.PodSpec{ + ReadinessGates: []v1.PodReadinessGate{}, + }, + } + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName, + }, + } + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + } + return fake.NewClientBuilder().WithRuntimeObjects(pod, node, svc).Build() + }, + }, + { + name: "with NotReadyAddresses with Pod containers not ready", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + NotReadyAddresses: []v1.EndpointAddress{ + { + IP: "10.0.0.3", + NodeName: &nodeName, + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Namespace: v1.NamespaceDefault, + Name: "test-pod-not-ready", + }, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.True(t, containsPotentialReady) + }, + setupKubeClient: func(t *testing.T, kubeClient client.Client) client.Client { + readinessGateName := helper.BuildReadinessGatePodConditionTypeWithPrefix(helper.TargetHealthPodConditionServiceTypePrefix, "test-svc") + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-not-ready", + Namespace: v1.NamespaceDefault, + }, + Spec: v1.PodSpec{ + ReadinessGates: []v1.PodReadinessGate{ + {ConditionType: readinessGateName}, + }, + }, + Status: v1.PodStatus{ + Conditions: []v1.PodCondition{ + { + Type: v1.ContainersReady, + Status: v1.ConditionFalse, + }, + }, + }, + } + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName, + }, + } + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + } + return fake.NewClientBuilder().WithRuntimeObjects(pod, node, svc).Build() + }, + }, + { + name: "port name not found in endpoints", + endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "10.0.0.1", + NodeName: &nodeName, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "other", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromString("tcp"), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testMgr := mgr + if tt.setupKubeClient != nil { + newKubeClient := tt.setupKubeClient(t, recon.kubeClient) + testMgr, err = NewServerGroupManager(newKubeClient, recon.cloud) + assert.NoError(t, err) + } + candidates := &reconbackend.EndpointWithENI{ + Endpoints: tt.endpoints, + TrafficPolicy: helper.ClusterTrafficPolicy, + AddressIPVersion: model.IPv4, + } + sg := nlbmodel.ServerGroup{ + ServicePort: tt.servicePort, + ServerGroupName: "test-sg", + AnyPortEnabled: tt.name == "anyPort enabled", + } + backends, containsPotentialReady, err := testMgr.setBackendsFromEndpoints(reqCtx, candidates, sg) + assert.NoError(t, err) + tt.validate(t, backends, containsPotentialReady) + }) + } +} + +func TestServerGroupManager_SetBackendsFromEndpointSlices(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + nodeName := "test-node" + portName := "tcp" + port := int32(8080) + protocol := v1.ProtocolTCP + ready := true + notReady := false + + tests := []struct { + name string + endpointSlices []discovery.EndpointSlice + servicePort *v1.ServicePort + validate func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) + }{ + { + name: "empty endpoint slices", + endpointSlices: []discovery.EndpointSlice{}, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + assert.False(t, containsPotentialReady) + }, + }, + { + name: "with ready endpoints", + endpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc-1", + Namespace: v1.NamespaceDefault, + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.1", "10.0.0.2"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + AddressType: discovery.AddressTypeIPv4, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromString("tcp"), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 2, len(backends)) + assert.Equal(t, "10.0.0.1", backends[0].ServerIp) + assert.Equal(t, "10.0.0.2", backends[1].ServerIp) + }, + }, + { + name: "with int targetPort", + endpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc-1", + Namespace: v1.NamespaceDefault, + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.1"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + AddressType: discovery.AddressTypeIPv4, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(9090), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 1, len(backends)) + assert.Equal(t, int32(9090), backends[0].Port) + }, + }, + { + name: "ignore terminating pods", + endpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc-1", + Namespace: v1.NamespaceDefault, + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.1"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + Terminating: &ready, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + AddressType: discovery.AddressTypeIPv4, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + }, + }, + { + name: "deduplicate endpoints", + endpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc-1", + Namespace: v1.NamespaceDefault, + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.1"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + }, + { + Addresses: []string{"10.0.0.1"}, + Conditions: discovery.EndpointConditions{ + Ready: &ready, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + AddressType: discovery.AddressTypeIPv4, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 1, len(backends)) + }, + }, + { + name: "not ready endpoint without targetRef", + endpointSlices: []discovery.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc-1", + Namespace: v1.NamespaceDefault, + }, + Endpoints: []discovery.Endpoint{ + { + Addresses: []string{"10.0.0.1"}, + Conditions: discovery.EndpointConditions{ + Ready: ¬Ready, + }, + NodeName: &nodeName, + }, + }, + Ports: []discovery.EndpointPort{ + { + Name: &portName, + Port: &port, + Protocol: &protocol, + }, + }, + AddressType: discovery.AddressTypeIPv4, + }, + }, + servicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + validate: func(t *testing.T, backends []nlbmodel.ServerGroupServer, containsPotentialReady bool) { + assert.Equal(t, 0, len(backends)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + candidates := &reconbackend.EndpointWithENI{ + EndpointSlices: tt.endpointSlices, + TrafficPolicy: helper.ClusterTrafficPolicy, + AddressIPVersion: model.IPv4, + } + sg := nlbmodel.ServerGroup{ + ServicePort: tt.servicePort, + ServerGroupName: "test-sg", + } + backends, containsPotentialReady, err := mgr.setBackendsFromEndpointSlices(reqCtx, candidates, sg) + assert.NoError(t, err) + tt.validate(t, backends, containsPotentialReady) + }) + } +} + +func TestServerGroupManager_BuildENIBackends(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + tests := []struct { + name string + initBackends []nlbmodel.ServerGroupServer + serverGroupType nlbmodel.ServerGroupType + weight *int + validate func(t *testing.T, result []nlbmodel.ServerGroupServer) + }{ + { + name: "empty backends", + initBackends: []nlbmodel.ServerGroupServer{}, + serverGroupType: nlbmodel.InstanceServerGroupType, + weight: nil, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 0, len(result)) + }, + }, + { + name: "with backends and InstanceServerGroupType", + initBackends: []nlbmodel.ServerGroupServer{ + {ServerIp: "10.0.0.1"}, + {ServerIp: "10.0.0.2"}, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + weight: nil, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, nlbmodel.EniServerType, result[0].ServerType) + assert.Equal(t, nlbmodel.EniServerType, result[1].ServerType) + assert.Equal(t, int32(100), result[0].Weight) + assert.Equal(t, int32(100), result[1].Weight) + }, + }, + { + name: "with backends and IpServerGroupType", + initBackends: []nlbmodel.ServerGroupServer{ + {ServerIp: "10.0.0.1"}, + }, + serverGroupType: nlbmodel.IpServerGroupType, + weight: nil, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 1, len(result)) + assert.Equal(t, nlbmodel.IpServerType, result[0].ServerType) + assert.Equal(t, "10.0.0.1", result[0].ServerId) + assert.Equal(t, "10.0.0.1", result[0].ServerIp) + }, + }, + { + name: "with weight", + initBackends: []nlbmodel.ServerGroupServer{ + {ServerIp: "10.0.0.1"}, + {ServerIp: "10.0.0.2"}, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + weight: intPtr(50), + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, int32(25), result[0].Weight) + assert.Equal(t, int32(25), result[1].Weight) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + candidates := &reconbackend.EndpointWithENI{ + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + } + sg := nlbmodel.ServerGroup{ + ServerGroupType: tt.serverGroupType, + Weight: tt.weight, + } + result, err := mgr.buildENIBackends(reqCtx, candidates, tt.initBackends, sg) + assert.NoError(t, err) + tt.validate(t, result) + }) + } +} + +func TestServerGroupManager_BuildLocalBackends(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + nodeName1 := NodeName + nodeName3 := "vk-node" + + tests := []struct { + name string + nodes []v1.Node + initBackends []nlbmodel.ServerGroupServer + serverGroupType nlbmodel.ServerGroupType + servicePort *v1.ServicePort + expectError bool + validate func(t *testing.T, result []nlbmodel.ServerGroupServer) + }{ + { + name: "empty backends", + nodes: []v1.Node{}, + initBackends: []nlbmodel.ServerGroupServer{}, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 0, len(result)) + }, + }, + { + name: "with ECS backends and InstanceServerGroupType", + nodes: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName1, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-id-1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + }, + }, + }, + }, + initBackends: []nlbmodel.ServerGroupServer{ + { + ServerIp: "10.0.0.1", + NodeName: &nodeName1, + }, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 1, len(result)) + assert.Equal(t, nlbmodel.EcsServerType, result[0].ServerType) + assert.Equal(t, "ecs-id-1", result[0].ServerId) + assert.Equal(t, int32(30080), result[0].Port) + }, + }, + { + name: "with ECS backends and IpServerGroupType", + nodes: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName1, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-id-1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + }, + }, + }, + }, + initBackends: []nlbmodel.ServerGroupServer{ + { + ServerIp: "10.0.0.1", + NodeName: &nodeName1, + }, + }, + serverGroupType: nlbmodel.IpServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 1, len(result)) + assert.Equal(t, nlbmodel.IpServerType, result[0].ServerType) + assert.Equal(t, "192.168.1.1", result[0].ServerId) + assert.Equal(t, "192.168.1.1", result[0].ServerIp) + }, + }, + { + name: "with VK node", + nodes: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName3, + Labels: map[string]string{ + "type": helper.LabelNodeTypeVK, + }, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-id-3", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.3"}, + }, + }, + }, + }, + initBackends: []nlbmodel.ServerGroupServer{ + { + ServerIp: "10.0.0.3", + NodeName: &nodeName3, + }, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 1, len(result)) + assert.Equal(t, nlbmodel.EniServerType, result[0].ServerType) + }, + }, + { + name: "node not found", + nodes: []v1.Node{}, + initBackends: []nlbmodel.ServerGroupServer{ + { + ServerIp: "10.0.0.1", + NodeName: &nodeName1, + }, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 0, len(result)) + }, + }, + { + name: "nil NodeName", + nodes: []v1.Node{}, + initBackends: []nlbmodel.ServerGroupServer{ + { + ServerIp: "10.0.0.1", + NodeName: nil, + }, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: true, + }, + { + name: "remove duplicated ECS", + nodes: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName1, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-id-1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + }, + }, + }, + }, + initBackends: []nlbmodel.ServerGroupServer{ + { + ServerIp: "10.0.0.1", + NodeName: &nodeName1, + }, + { + ServerIp: "10.0.0.2", + NodeName: &nodeName1, + }, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 1, len(result)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + candidates := &reconbackend.EndpointWithENI{ + Nodes: tt.nodes, + TrafficPolicy: helper.LocalTrafficPolicy, + AddressIPVersion: model.IPv4, + } + sg := nlbmodel.ServerGroup{ + ServerGroupType: tt.serverGroupType, + ServicePort: tt.servicePort, + ServerGroupName: "test-sg", + } + result, err := mgr.buildLocalBackends(reqCtx, candidates, tt.initBackends, sg) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, result) + } + } + }) + } +} + +func TestServerGroupManager_BuildClusterBackends(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.TODO(), + Service: svc, + Anno: annotation.NewAnnotationRequest(svc), + Log: util.NLBLog.WithValues("service", util.Key(svc)), + } + + nodeName1 := NodeName + nodeName2 := "cn-hangzhou.192.0.168.69" + nodeName3 := "vk-node" + + tests := []struct { + name string + nodes []v1.Node + initBackends []nlbmodel.ServerGroupServer + serverGroupType nlbmodel.ServerGroupType + servicePort *v1.ServicePort + expectError bool + validate func(t *testing.T, result []nlbmodel.ServerGroupServer) + }{ + { + name: "with nodes and InstanceServerGroupType", + nodes: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName1, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-id-1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName2, + }, + Spec: v1.NodeSpec{ + ProviderID: "alicloud://cn-hangzhou.ecs-id-2", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.2"}, + }, + }, + }, + }, + initBackends: []nlbmodel.ServerGroupServer{}, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 2, len(result)) + assert.Equal(t, nlbmodel.EcsServerType, result[0].ServerType) + assert.Equal(t, nlbmodel.EcsServerType, result[1].ServerType) + assert.Equal(t, int32(30080), result[0].Port) + assert.Equal(t, int32(30080), result[1].Port) + }, + }, + { + name: "with nodes and IpServerGroupType", + nodes: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName1, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-id-1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + }, + }, + }, + }, + initBackends: []nlbmodel.ServerGroupServer{}, + serverGroupType: nlbmodel.IpServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 1, len(result)) + assert.Equal(t, nlbmodel.IpServerType, result[0].ServerType) + assert.Equal(t, "192.168.1.1", result[0].ServerId) + assert.Equal(t, "192.168.1.1", result[0].ServerIp) + }, + }, + { + name: "with VK backends", + nodes: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName3, + Labels: map[string]string{ + "type": helper.LabelNodeTypeVK, + }, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-id-3", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.3"}, + }, + }, + }, + }, + initBackends: []nlbmodel.ServerGroupServer{ + { + ServerIp: "10.0.0.3", + NodeName: &nodeName3, + }, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.True(t, len(result) >= 1) + hasEniBackend := false + for _, b := range result { + if b.ServerType == nlbmodel.EniServerType { + hasEniBackend = true + break + } + } + assert.True(t, hasEniBackend) + }, + }, + { + name: "nil NodeName in initBackends", + nodes: []v1.Node{}, + initBackends: []nlbmodel.ServerGroupServer{ + { + ServerIp: "10.0.0.1", + NodeName: nil, + }, + }, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: true, + }, + { + name: "empty nodes", + nodes: []v1.Node{}, + initBackends: []nlbmodel.ServerGroupServer{}, + serverGroupType: nlbmodel.InstanceServerGroupType, + servicePort: &v1.ServicePort{ + NodePort: 30080, + }, + expectError: false, + validate: func(t *testing.T, result []nlbmodel.ServerGroupServer) { + assert.Equal(t, 0, len(result)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + candidates := &reconbackend.EndpointWithENI{ + Nodes: tt.nodes, + TrafficPolicy: helper.ClusterTrafficPolicy, + AddressIPVersion: model.IPv4, + } + sg := nlbmodel.ServerGroup{ + ServerGroupType: tt.serverGroupType, + ServicePort: tt.servicePort, + ServerGroupName: "test-sg", + } + result, err := mgr.buildClusterBackends(reqCtx, candidates, tt.initBackends, sg) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, result) + } + } + }) + } +} + +func TestServerGroupManager_SetServerGroupServers(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + nodeName := NodeName + node := v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName, + }, + Spec: v1.NodeSpec{ + ProviderID: "cn-hangzhou.ecs-id-1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeInternalIP, Address: "192.168.1.1"}, + }, + }, + } + + endpoints := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "10.0.0.1", + NodeName: &nodeName, + }, + }, + Ports: []v1.EndpointPort{ + { + Name: "tcp", + Port: 8080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + } + + tests := []struct { + name string + svc *v1.Service + candidates *reconbackend.EndpointWithENI + sg *nlbmodel.ServerGroup + isUserManagedLB bool + expectError bool + validate func(t *testing.T, sg *nlbmodel.ServerGroup, containsPotentialReady bool) + }{ + { + name: "ENI traffic policy", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.BackendType): "eni", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + NamedKey: &nlbmodel.SGNamedKey{ + NamedKey: nlbmodel.NamedKey{ + ServiceName: "test-svc", + Namespace: v1.NamespaceDefault, + }, + }, + }, + isUserManagedLB: false, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup, containsPotentialReady bool) { + assert.True(t, len(sg.Servers) >= 0) + }, + }, + { + name: "Local traffic policy", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal, + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + NodePort: 30080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.LocalTrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + NodePort: 30080, + }, + ServerGroupName: "test-sg", + NamedKey: &nlbmodel.SGNamedKey{ + NamedKey: nlbmodel.NamedKey{ + ServiceName: "test-svc", + Namespace: v1.NamespaceDefault, + }, + }, + }, + isUserManagedLB: false, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup, containsPotentialReady bool) { + assert.True(t, len(sg.Servers) >= 0) + }, + }, + { + name: "Cluster traffic policy", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + NodePort: 30080, + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ClusterTrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + NodePort: 30080, + }, + ServerGroupName: "test-sg", + NamedKey: &nlbmodel.SGNamedKey{ + NamedKey: nlbmodel.NamedKey{ + ServiceName: "test-svc", + Namespace: v1.NamespaceDefault, + }, + }, + }, + isUserManagedLB: false, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup, containsPotentialReady bool) { + assert.True(t, len(sg.Servers) >= 0) + }, + }, + { + name: "unsupported traffic policy", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.TrafficPolicy("unsupported"), + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: false, + expectError: true, + }, + { + name: "user managed LB with VGroupWeight", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.VGroupWeight): "50", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: true, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup, containsPotentialReady bool) { + if sg.Weight != nil { + assert.Equal(t, 50, *sg.Weight) + } + }, + }, + { + name: "user managed LB with invalid VGroupPort format", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.VGroupPort): "invalid-format", + annotation.Annotation(annotation.LoadBalancerId): "nlb-exist-id", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: true, + expectError: true, + }, + { + name: "user managed LB with VGroupPort not found", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.VGroupPort): "sg-not-found-id:80", + annotation.Annotation(annotation.LoadBalancerId): "nlb-exist-id", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: true, + expectError: true, + }, + { + name: "user managed LB with VGroupPort not user managed", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.VGroupPort): "sg-not-user-managed-id:80", + annotation.Annotation(annotation.LoadBalancerId): "nlb-exist-id", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: true, + expectError: true, + }, + { + name: "user managed LB with valid VGroupPort", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.VGroupPort): "sg-user-managed-id:80", + annotation.Annotation(annotation.LoadBalancerId): "nlb-exist-id", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: true, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup, containsPotentialReady bool) { + assert.Equal(t, "sg-user-managed-id", sg.ServerGroupId) + assert.True(t, sg.IsUserManaged) + }, + }, + { + name: "user managed LB with invalid VGroupWeight - non-numeric", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.VGroupPort): "sg-user-managed-id:80", + annotation.Annotation(annotation.VGroupWeight): "invalid", + annotation.Annotation(annotation.LoadBalancerId): "nlb-exist-id", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: true, + expectError: true, + }, + { + name: "user managed LB with invalid VGroupWeight - out of range (negative)", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.VGroupPort): "sg-user-managed-id:80", + annotation.Annotation(annotation.VGroupWeight): "-1", + annotation.Annotation(annotation.LoadBalancerId): "nlb-exist-id", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: true, + expectError: true, + }, + { + name: "user managed LB with invalid VGroupWeight - out of range (over 100)", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.VGroupPort): "sg-user-managed-id:80", + annotation.Annotation(annotation.VGroupWeight): "101", + annotation.Annotation(annotation.LoadBalancerId): "nlb-exist-id", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: endpoints, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + }, + isUserManagedLB: true, + expectError: true, + }, + { + name: "empty backends triggers event", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + candidates: &reconbackend.EndpointWithENI{ + Endpoints: &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: v1.NamespaceDefault, + }, + Subsets: []v1.EndpointSubset{}, + }, + Nodes: []v1.Node{node}, + TrafficPolicy: helper.ENITrafficPolicy, + AddressIPVersion: model.IPv4, + }, + sg: &nlbmodel.ServerGroup{ + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + ServerGroupName: "test-sg", + NamedKey: &nlbmodel.SGNamedKey{ + NamedKey: nlbmodel.NamedKey{ + ServiceName: "test-svc", + Namespace: v1.NamespaceDefault, + }, + }, + }, + isUserManagedLB: false, + expectError: false, + validate: func(t *testing.T, sg *nlbmodel.ServerGroup, containsPotentialReady bool) { + assert.Equal(t, 0, len(sg.Servers)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reqCtx := getReqCtx(tt.svc) + if reqCtx.Recorder == nil { + reqCtx.Recorder = record.NewFakeRecorder(10) + } + containsPotentialReady, err := mgr.setServerGroupServers(reqCtx, tt.sg, tt.candidates, tt.isUserManagedLB) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, tt.sg, containsPotentialReady) + } + } + }) + } +} + +func TestServerGroupManager_BuildLocalModel(t *testing.T) { + recon, err := getReconcileNLB() + assert.NoError(t, err) + + mgr, err := NewServerGroupManager(recon.kubeClient, recon.cloud) + assert.NoError(t, err) + + tests := []struct { + name string + svc *v1.Service + mdl *nlbmodel.NetworkLoadBalancer + expectError bool + validate func(t *testing.T, mdl *nlbmodel.NetworkLoadBalancer) + }{ + { + name: "build local model with normal listener", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + mdl: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + IsUserManaged: false, + }, + Listeners: []*nlbmodel.ListenerAttribute{ + { + ListenerPort: 80, + ListenerProtocol: nlbmodel.TCP, + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + expectError: false, + validate: func(t *testing.T, mdl *nlbmodel.NetworkLoadBalancer) { + assert.NotNil(t, mdl.ServerGroups) + assert.True(t, len(mdl.ServerGroups) > 0) + }, + }, + { + name: "build local model with anyPort listener", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + mdl: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + IsUserManaged: false, + }, + Listeners: []*nlbmodel.ListenerAttribute{ + { + ListenerPort: 0, + StartPort: 1000, + EndPort: 2000, + ListenerProtocol: nlbmodel.TCP, + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + expectError: false, + validate: func(t *testing.T, mdl *nlbmodel.NetworkLoadBalancer) { + assert.NotNil(t, mdl.ServerGroups) + assert.True(t, len(mdl.ServerGroups) > 0) + if len(mdl.ServerGroups) > 0 { + assert.True(t, mdl.ServerGroups[0].AnyPortEnabled) + assert.NotNil(t, mdl.ServerGroups[0].HealthCheckConfig) + } + }, + }, + { + name: "build local model with invalid annotation", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: v1.NamespaceDefault, + Name: "test-svc", + Annotations: map[string]string{ + annotation.Annotation(annotation.ServerGroupType): "invalid", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + mdl: &nlbmodel.NetworkLoadBalancer{ + LoadBalancerAttribute: &nlbmodel.LoadBalancerAttribute{ + IsUserManaged: false, + }, + Listeners: []*nlbmodel.ListenerAttribute{ + { + ListenerPort: 80, + ListenerProtocol: nlbmodel.TCP, + ServicePort: &v1.ServicePort{ + Name: "tcp", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: v1.ProtocolTCP, + }, + }, + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reqCtx := getReqCtx(tt.svc) + reqCtx.Recorder = record.NewFakeRecorder(10) + + mdlCopy := *tt.mdl + err := mgr.BuildLocalModel(reqCtx, &mdlCopy) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, &mdlCopy) + } + } + }) + } +} + +func TestAddressTypeToIPVersionType(t *testing.T) { + assert.Equal(t, model.IPv4, addressTypeToIPVersionType(discovery.AddressTypeIPv4)) + assert.Equal(t, model.IPv6, addressTypeToIPVersionType(discovery.AddressTypeIPv6)) + assert.Equal(t, model.IPv4, addressTypeToIPVersionType(discovery.AddressTypeFQDN)) +} diff --git a/pkg/controller/service/reconcile/annotation/annotations_test.go b/pkg/controller/service/reconcile/annotation/annotations_test.go index cf231f2cb..dd5659cbd 100644 --- a/pkg/controller/service/reconcile/annotation/annotations_test.go +++ b/pkg/controller/service/reconcile/annotation/annotations_test.go @@ -1,11 +1,13 @@ package annotation import ( + "testing" + "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - "testing" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model/tag" ) func TestGet(t *testing.T) { @@ -57,6 +59,108 @@ func TestGetDefaultLoadBalancerName(t *testing.T) { assert.Equal(t, anno.GetDefaultLoadBalancerName(), "a5e4dbfc9c2ae4642b0335607860aef6") } +func TestHas(t *testing.T) { + svc := getDefaultService() + anno := NewAnnotationRequest(svc) + + assert.False(t, anno.Has(AddressType)) + + svc.Annotations[Annotation(AddressType)] = "intranet" + assert.True(t, anno.Has(AddressType)) + + svc = getDefaultService() + anno = NewAnnotationRequest(svc) + svc.Annotations["service.beta.kubernetes.io/alicloud-loadbalancer-address-type"] = "internet" + assert.True(t, anno.Has(AddressType)) + + svc = getDefaultService() + anno = NewAnnotationRequest(svc) + svc.Annotations[Annotation(AddressType)] = "Intranet" + svc.Annotations["service.beta.kubernetes.io/alicloud-loadbalancer-address-type"] = "internet" + assert.True(t, anno.Has(AddressType)) +} + +func TestGetDefaultTags(t *testing.T) { + svc := getDefaultService() + svc.UID = "5e4dbfc9-c2ae-4642-b033-5607860aef6a" + anno := NewAnnotationRequest(svc) + tags := anno.GetDefaultTags() + + assert.Equal(t, 2, len(tags)) + + assert.Equal(t, "kubernetes.do.not.delete", tags[0].Key) + assert.Equal(t, "a5e4dbfc9c2ae4642b0335607860aef6", tags[0].Value) + + assert.Equal(t, "ack.aliyun.com", tags[1].Key) +} + +func TestGetLoadBalancerAdditionalTagsEdgeCases(t *testing.T) { + svc := getDefaultService() + anno := NewAnnotationRequest(svc) + + svc.Annotations[Annotation(AdditionalTags)] = "" + tags := anno.GetLoadBalancerAdditionalTags() + assert.Equal(t, 0, len(tags)) + + svc.Annotations[Annotation(AdditionalTags)] = "Key1=Value1,Key2=,Key3" + tags = anno.GetLoadBalancerAdditionalTags() + assert.Equal(t, 3, len(tags)) + + var key1Tag, key2Tag, key3Tag *tag.Tag + for _, tag := range tags { + if tag.Key == "Key1" { + key1Tag = &tag + } else if tag.Key == "Key2" { + key2Tag = &tag + } else if tag.Key == "Key3" { + key3Tag = &tag + } + } + + assert.NotNil(t, key1Tag) + assert.Equal(t, "Value1", key1Tag.Value) + + assert.NotNil(t, key2Tag) + assert.Equal(t, "", key2Tag.Value) + + assert.NotNil(t, key3Tag) + assert.Equal(t, "", key3Tag.Value) + + svc.Annotations[Annotation(AdditionalTags)] = " Key1 = Value1 , Key2 =Value2,Key3= Value3 " + tags = anno.GetLoadBalancerAdditionalTags() + assert.Equal(t, 3, len(tags)) +} + +func TestGetWithNilServiceOrAnnotations(t *testing.T) { + anno := &AnnotationRequest{Service: nil} + assert.Equal(t, "", anno.Get(AddressType)) + + svc := getDefaultService() + svc.Annotations = nil + anno = NewAnnotationRequest(svc) + assert.Equal(t, "", anno.Get(AddressType)) +} + +func TestHasWithNilServiceOrAnnotations(t *testing.T) { + anno := &AnnotationRequest{Service: nil} + assert.False(t, anno.Has(AddressType)) + + svc := getDefaultService() + svc.Annotations = nil + anno = NewAnnotationRequest(svc) + assert.False(t, anno.Has(AddressType)) +} + +func TestGetDefaultValueForLoadBalancerName(t *testing.T) { + svc := getDefaultService() + svc.UID = "5e4dbfc9-c2ae-4642-b033-5607860aef6a" + anno := NewAnnotationRequest(svc) + + name := anno.GetDefaultValue(LoadBalancerName) + expectedName := "a5e4dbfc9c2ae4642b0335607860aef6" + assert.Equal(t, expectedName, name) +} + func getDefaultService() *v1.Service { return &v1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -78,3 +182,121 @@ func getDefaultService() *v1.Service { }, } } + +func TestAnnotationRequest_IsForceOverride(t *testing.T) { + tests := []struct { + name string + svc *v1.Service + expected bool + description string + }{ + { + name: "annotation value is true", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-force-override-listeners": "true", + }, + }, + }, + expected: true, + description: "should return true when annotation value is 'true'", + }, + { + name: "annotation value is false", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-force-override-listeners": "false", + }, + }, + }, + expected: false, + description: "should return false when annotation value is 'false'", + }, + { + name: "annotation value is empty string", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-force-override-listeners": "", + }, + }, + }, + expected: false, + description: "should return false when annotation value is empty", + }, + { + name: "annotation does not exist", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "some-other-annotation": "value", + }, + }, + }, + expected: false, + description: "should return false when annotation does not exist", + }, + { + name: "annotations map is nil", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: nil, + }, + }, + expected: false, + description: "should return false when annotations map is nil", + }, + { + name: "service is nil", + svc: nil, + expected: false, + description: "should return false when service is nil", + }, + { + name: "legacy annotation prefix with true value", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/alicloud-loadbalancer-force-override-listeners": "true", + }, + }, + }, + expected: true, + description: "should return true when legacy annotation is 'true'", + }, + { + name: "annotation value is 'True' (uppercase)", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-force-override-listeners": "True", + }, + }, + }, + expected: false, + description: "should return false when annotation value is 'True' (not lowercase 'true')", + }, + { + name: "annotation value is '1'", + svc: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-force-override-listeners": "1", + }, + }, + }, + expected: false, + description: "should return false when annotation value is '1' instead of 'true'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := NewAnnotationRequest(tt.svc) + result := req.IsForceOverride() + assert.Equal(t, tt.expected, result, tt.description) + }) + } +} diff --git a/pkg/controller/service/reconcile/backend/backend_test.go b/pkg/controller/service/reconcile/backend/backend_test.go index 00bfc12ab..bc1d100c1 100644 --- a/pkg/controller/service/reconcile/backend/backend_test.go +++ b/pkg/controller/service/reconcile/backend/backend_test.go @@ -1,9 +1,22 @@ package backend import ( + "context" "testing" "github.com/stretchr/testify/assert" + + v1 "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/helper" + "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/annotation" + svcCtx "k8s.io/cloud-provider-alibaba-cloud/pkg/controller/service/reconcile/context" + "k8s.io/cloud-provider-alibaba-cloud/pkg/model" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestBatch(t *testing.T) { @@ -20,3 +33,820 @@ func TestBatch(t *testing.T) { } assert.Equal(t, sum, 55) } + +func TestFilterOutByLabel(t *testing.T) { + nodes := []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + "environment": "production", + "zone": "zone1", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + Labels: map[string]string{ + "environment": "production", + "zone": "zone2", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node3", + Labels: map[string]string{ + "environment": "development", + "zone": "zone1", + }, + }, + }, + } + + t.Run("filter by single label", func(t *testing.T) { + result, err := filterOutByLabel(nodes, "environment=production") + assert.NoError(t, err) + assert.Len(t, result, 2) + assert.Contains(t, []string{result[0].Name, result[1].Name}, "node1") + assert.Contains(t, []string{result[0].Name, result[1].Name}, "node2") + }) + + t.Run("filter by multiple labels", func(t *testing.T) { + result, err := filterOutByLabel(nodes, "environment=production,zone=zone1") + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "node1", result[0].Name) + }) + + t.Run("filter by non-existent label", func(t *testing.T) { + result, err := filterOutByLabel(nodes, "environment=staging") + assert.NoError(t, err) + assert.Len(t, result, 0) + }) + + t.Run("invalid label format", func(t *testing.T) { + _, err := filterOutByLabel(nodes, "environment") + assert.Error(t, err) + }) + + t.Run("empty label string", func(t *testing.T) { + result, err := filterOutByLabel(nodes, "") + assert.NoError(t, err) + assert.Len(t, result, 3) + }) +} + +func TestNeedExcludeFromLB(t *testing.T) { + t.Run("master node", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + masterNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "master-node", + Labels: map[string]string{ + "node-role.kubernetes.io/master": "", + }, + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + assert.True(t, needExcludeFromLB(reqCtx, masterNode)) + }) + + t.Run("node with ToBeDeletedTaint", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-to-be-deleted", + }, + Spec: v1.NodeSpec{ + Taints: []v1.Taint{ + { + Key: "ToBeDeletedByClusterAutoscaler", + Value: "some-value", + }, + }, + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + assert.True(t, needExcludeFromLB(reqCtx, node)) + }) + + t.Run("unschedulable node with remove-unscheduled-backend on", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-remove-unscheduled-backend": "on", + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unschedulable-node", + }, + Spec: v1.NodeSpec{ + Unschedulable: true, + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + assert.True(t, needExcludeFromLB(reqCtx, node)) + }) + + t.Run("unschedulable node with remove-unscheduled-backend not on", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-remove-unscheduled-backend": "off", + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unschedulable-node", + }, + Spec: v1.NodeSpec{ + Unschedulable: true, + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + assert.False(t, needExcludeFromLB(reqCtx, node)) + }) + + t.Run("unschedulable node without remove-unscheduled-backend annotation", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + Annotations: map[string]string{}, + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unschedulable-node", + }, + Spec: v1.NodeSpec{ + Unschedulable: true, + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + assert.False(t, needExcludeFromLB(reqCtx, node)) + }) + + t.Run("node without conditions", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-without-conditions", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{}, + }, + } + assert.True(t, needExcludeFromLB(reqCtx, node)) + }) + + t.Run("not ready node", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-ready-node", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionFalse, + }, + }, + }, + } + assert.True(t, needExcludeFromLB(reqCtx, node)) + }) + + t.Run("vk node", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vk-node", + Labels: map[string]string{ + "type": "virtual-kubelet", + }, + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + assert.False(t, needExcludeFromLB(reqCtx, node)) + }) + + t.Run("normal ready node", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "normal-node", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + } + assert.False(t, needExcludeFromLB(reqCtx, node)) + }) +} + +func TestSetTrafficPolicy(t *testing.T) { + // Create a mock EndpointWithENI + ep := &EndpointWithENI{} + + t.Run("eni backend type", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/backend-type": "eni", + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + ep.setTrafficPolicy(reqCtx) + assert.Equal(t, helper.ENITrafficPolicy, ep.TrafficPolicy) + }) + + t.Run("local traffic policy", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal, + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + ep.setTrafficPolicy(reqCtx) + assert.Equal(t, helper.LocalTrafficPolicy, ep.TrafficPolicy) + }) + + t.Run("cluster traffic policy", func(t *testing.T) { + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + ep.setTrafficPolicy(reqCtx) + assert.Equal(t, helper.ClusterTrafficPolicy, ep.TrafficPolicy) + }) +} + +func TestSetAddressIPVersion(t *testing.T) { + t.Run("default ipv4", func(t *testing.T) { + ep := &EndpointWithENI{} + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + ep.setAddressIpVersion(reqCtx) + assert.Equal(t, model.IPv4, ep.AddressIPVersion) + }) + + t.Run("ipv6 backend version with missing feature gates", func(t *testing.T) { + ep := &EndpointWithENI{} + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-backend-ip-version": "ipv6", + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + ep.setAddressIpVersion(reqCtx) + assert.Equal(t, model.IPv4, ep.AddressIPVersion) + }) + + t.Run("ipv6 backend version with feature gates but no ip version annotation", func(t *testing.T) { + ep := &EndpointWithENI{} + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-backend-ip-version": "ipv6", + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + _ = utilfeature.DefaultMutableFeatureGate.Set("IPv6DualStack=true") + _ = utilfeature.DefaultMutableFeatureGate.Set("EndpointSlice=true") + defer func() { + _ = utilfeature.DefaultMutableFeatureGate.Set("IPv6DualStack=false") + _ = utilfeature.DefaultMutableFeatureGate.Set("EndpointSlice=false") + }() + + ep.setAddressIpVersion(reqCtx) + assert.Equal(t, model.IPv4, ep.AddressIPVersion) + }) + + t.Run("ipv6 backend version with all conditions met", func(t *testing.T) { + ep := &EndpointWithENI{} + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-backend-ip-version": "ipv6", + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-ip-version": "ipv6", + }, + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + _ = utilfeature.DefaultMutableFeatureGate.Set("IPv6DualStack=true") + _ = utilfeature.DefaultMutableFeatureGate.Set("EndpointSlice=true") + defer func() { + _ = utilfeature.DefaultMutableFeatureGate.Set("IPv6DualStack=false") + _ = utilfeature.DefaultMutableFeatureGate.Set("EndpointSlice=false") + }() + + ep.setAddressIpVersion(reqCtx) + assert.Equal(t, model.IPv6, ep.AddressIPVersion) + }) + + t.Run("dualstack for nlb with all conditions met", func(t *testing.T) { + ep := &EndpointWithENI{} + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-backend-ip-version": "ipv6", + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-ip-version": "dualstack", + }, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeLoadBalancer, + LoadBalancerClass: pointer.String(helper.NLBClass), + }, + } + reqCtx := &svcCtx.RequestContext{ + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + _ = utilfeature.DefaultMutableFeatureGate.Set("IPv6DualStack=true") + _ = utilfeature.DefaultMutableFeatureGate.Set("EndpointSlice=true") + defer func() { + _ = utilfeature.DefaultMutableFeatureGate.Set("IPv6DualStack=false") + _ = utilfeature.DefaultMutableFeatureGate.Set("EndpointSlice=false") + }() + + ep.setAddressIpVersion(reqCtx) + assert.Equal(t, model.IPv6, ep.AddressIPVersion) + }) +} + +func TestNewEndpointWithENI(t *testing.T) { + nodeList := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + }, + }, + }, + }, + }, + } + + endpoints := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Subsets: []v1.EndpointSubset{ + { + Addresses: []v1.EndpointAddress{ + { + IP: "192.168.1.1", + }, + }, + Ports: []v1.EndpointPort{ + { + Port: 80, + }, + }, + }, + }, + } + + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + } + + scheme := runtime.NewScheme() + _ = v1.AddToScheme(scheme) + _ = discovery.AddToScheme(scheme) + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(endpoints, service). + WithLists(nodeList). + Build() + + t.Run("successfully create EndpointWithENI", func(t *testing.T) { + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + result, err := NewEndpointWithENI(reqCtx, fakeClient) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result.Nodes, 2) + assert.Equal(t, helper.ClusterTrafficPolicy, result.TrafficPolicy) + assert.Equal(t, model.IPv4, result.AddressIPVersion) + }) + + t.Run("using endpoint", func(t *testing.T) { + err := utilfeature.DefaultMutableFeatureGate.Set("EndpointSlice=false") + assert.NoError(t, err) + defer func() { + err := utilfeature.DefaultMutableFeatureGate.Set("EndpointSlice=true") + assert.NoError(t, err) + }() + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: service, + Anno: annotation.NewAnnotationRequest(service), + } + + result, err := NewEndpointWithENI(reqCtx, fakeClient) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result.Nodes, 2) + assert.Equal(t, helper.ClusterTrafficPolicy, result.TrafficPolicy) + assert.Equal(t, model.IPv4, result.AddressIPVersion) + }) + + t.Run("with eni backend type", func(t *testing.T) { + eniService := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eni-service", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/backend-type": "eni", + }, + }, + } + + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: eniService, + Anno: annotation.NewAnnotationRequest(eniService), + } + + result, err := NewEndpointWithENI(reqCtx, fakeClient) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, helper.ENITrafficPolicy, result.TrafficPolicy) + }) + + t.Run("with local external traffic policy", func(t *testing.T) { + localService := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "local-service", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal, + }, + } + + reqCtx := &svcCtx.RequestContext{ + Ctx: context.Background(), + Service: localService, + Anno: annotation.NewAnnotationRequest(localService), + } + + result, err := NewEndpointWithENI(reqCtx, fakeClient) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, helper.LocalTrafficPolicy, result.TrafficPolicy) + }) +} + +func TestGetNodes(t *testing.T) { + scheme := runtime.NewScheme() + _ = v1.AddToScheme(scheme) + readyNode := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "node-1"}, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + {Type: v1.NodeReady, Status: v1.ConditionTrue}, + }, + }, + } + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "svc", Namespace: "default"}, + } + t.Run("success", func(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme).WithLists(&v1.NodeList{Items: []v1.Node{*readyNode}}).Build() + reqCtx := &svcCtx.RequestContext{Ctx: context.Background(), Service: service, Anno: annotation.NewAnnotationRequest(service)} + nodes, err := GetNodes(reqCtx, client) + assert.NoError(t, err) + assert.Len(t, nodes, 1) + assert.Equal(t, "node-1", nodes[0].Name) + }) + t.Run("with backend label filter", func(t *testing.T) { + labeledNode := readyNode.DeepCopy() + labeledNode.Labels = map[string]string{"env": "prod"} + client := fake.NewClientBuilder().WithScheme(scheme).WithLists(&v1.NodeList{Items: []v1.Node{*labeledNode}}).Build() + svcWithAnno := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc", + Namespace: "default", + Annotations: map[string]string{ + "service.beta.kubernetes.io/alibaba-cloud-loadbalancer-backend-label": "env=prod", + }, + }, + } + reqCtx := &svcCtx.RequestContext{Ctx: context.Background(), Service: svcWithAnno, Anno: annotation.NewAnnotationRequest(svcWithAnno)} + nodes, err := GetNodes(reqCtx, client) + assert.NoError(t, err) + assert.Len(t, nodes, 1) + }) + t.Run("empty list", func(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme).Build() + reqCtx := &svcCtx.RequestContext{Ctx: context.Background(), Service: service, Anno: annotation.NewAnnotationRequest(service)} + nodes, err := GetNodes(reqCtx, client) + assert.NoError(t, err) + assert.Len(t, nodes, 0) + }) +} + +func TestGetEndpoints(t *testing.T) { + scheme := runtime.NewScheme() + _ = v1.AddToScheme(scheme) + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "svc", Namespace: "default"}, + } + eps := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{Name: "svc", Namespace: "default"}, + Subsets: []v1.EndpointSubset{{Addresses: []v1.EndpointAddress{{IP: "10.0.0.1"}}}}, + } + t.Run("found", func(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(eps).Build() + reqCtx := &svcCtx.RequestContext{Ctx: context.Background(), Service: service, Anno: annotation.NewAnnotationRequest(service)} + result, err := getEndpoints(reqCtx, client) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result.Subsets, 1) + }) + t.Run("not found", func(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme).Build() + reqCtx := &svcCtx.RequestContext{Ctx: context.Background(), Service: service, Anno: annotation.NewAnnotationRequest(service)} + result, err := getEndpoints(reqCtx, client) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Len(t, result.Subsets, 0) + }) +} + +func TestGetEndpointByEndpointSlice(t *testing.T) { + scheme := runtime.NewScheme() + _ = v1.AddToScheme(scheme) + _ = discovery.AddToScheme(scheme) + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "svc", Namespace: "default"}, + } + esIPv4 := &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + Labels: map[string]string{discovery.LabelServiceName: "svc"}, + }, + AddressType: discovery.AddressTypeIPv4, + Endpoints: []discovery.Endpoint{{Addresses: []string{"10.0.0.1"}}}, + } + esIPv6 := &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-2", + Namespace: "default", + Labels: map[string]string{discovery.LabelServiceName: "svc"}, + }, + AddressType: discovery.AddressTypeIPv6, + Endpoints: []discovery.Endpoint{{Addresses: []string{"fd00::1"}}}, + } + t.Run("ipv4", func(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(esIPv4).Build() + reqCtx := &svcCtx.RequestContext{Ctx: context.Background(), Service: service, Anno: annotation.NewAnnotationRequest(service)} + result, err := getEndpointByEndpointSlice(reqCtx, client, model.IPv4) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, discovery.AddressTypeIPv4, result[0].AddressType) + }) + t.Run("ipv6", func(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(esIPv6).Build() + reqCtx := &svcCtx.RequestContext{Ctx: context.Background(), Service: service, Anno: annotation.NewAnnotationRequest(service)} + result, err := getEndpointByEndpointSlice(reqCtx, client, model.IPv6) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, discovery.AddressTypeIPv6, result[0].AddressType) + }) +} diff --git a/pkg/model/load_balancer.go b/pkg/model/load_balancer.go index b9372d8d1..eaef94042 100644 --- a/pkg/model/load_balancer.go +++ b/pkg/model/load_balancer.go @@ -13,7 +13,10 @@ import ( type ListenerStatus string -const Stopped = ListenerStatus("stopped") +const ( + Stopped = ListenerStatus("stopped") + Running = ListenerStatus("running") +) type AddressType string diff --git a/pkg/model/load_balancer_test.go b/pkg/model/load_balancer_test.go index a25ad2dc4..b55a9b33d 100644 --- a/pkg/model/load_balancer_test.go +++ b/pkg/model/load_balancer_test.go @@ -1,6 +1,7 @@ package model import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -62,3 +63,330 @@ func TestParseFlagType(t *testing.T) { } } } + +func TestInstanceChargeType_IsPayBySpec(t *testing.T) { + tests := []struct { + name string + t InstanceChargeType + want bool + }{ + { + name: "PayBySpec uppercase", + t: PayBySpec, + want: true, + }, + { + name: "PayBySpec lowercase", + t: "paybyspec", + want: true, + }, + { + name: "PayByCLCU", + t: PayByCLCU, + want: false, + }, + { + name: "empty string", + t: "", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.t.IsPayBySpec()) + }) + } +} + +func TestInstanceChargeType_IsPayByCLCU(t *testing.T) { + tests := []struct { + name string + t InstanceChargeType + want bool + }{ + { + name: "PayByCLCU uppercase", + t: PayByCLCU, + want: true, + }, + { + name: "PayByCLCU lowercase", + t: "paybyclcu", + want: true, + }, + { + name: "empty string", + t: "", + want: true, + }, + { + name: "PayBySpec", + t: PayBySpec, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.t.IsPayByCLCU()) + }) + } +} + +func TestVServerGroup_BackendInfo(t *testing.T) { + tests := []struct { + name string + backends []BackendAttribute + wantJSON bool + }{ + { + name: "few backends", + backends: []BackendAttribute{ + {ServerId: "i-123", ServerIp: "192.168.1.1", Port: 80, Weight: 100}, + {ServerId: "i-456", ServerIp: "192.168.1.2", Port: 80, Weight: 100}, + }, + wantJSON: true, + }, + { + name: "empty backends", + backends: []BackendAttribute{}, + wantJSON: true, + }, + { + name: "more than 100 backends", + backends: func() []BackendAttribute { + var backends []BackendAttribute + for i := 0; i < 150; i++ { + backends = append(backends, BackendAttribute{ + ServerId: "i-" + string(rune(i)), + Port: 80, + }) + } + return backends + }(), + wantJSON: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vg := &VServerGroup{Backends: tt.backends} + result := vg.BackendInfo() + assert.NotEmpty(t, result) + + if tt.wantJSON { + var backends []BackendAttribute + err := json.Unmarshal([]byte(result), &backends) + assert.NoError(t, err) + if len(tt.backends) <= 100 { + assert.Equal(t, len(tt.backends), len(backends)) + } else { + assert.Equal(t, 100, len(backends)) + } + } + }) + } +} + +func TestListenerNamedKey_String(t *testing.T) { + tests := []struct { + name string + key *ListenerNamedKey + want string + }{ + { + name: "nil key", + key: nil, + want: "", + }, + { + name: "valid key with default prefix", + key: &ListenerNamedKey{ + CID: "c123", + Namespace: "default", + ServiceName: "nginx", + Port: 80, + }, + want: "k8s/80/nginx/default/c123", + }, + { + name: "valid key with custom prefix", + key: &ListenerNamedKey{ + Prefix: "custom", + CID: "c123", + Namespace: "default", + ServiceName: "nginx", + Port: 80, + }, + want: "custom/80/nginx/default/c123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.key.String()) + }) + } +} + +func TestListenerNamedKey_Key(t *testing.T) { + key := &ListenerNamedKey{ + CID: "c123", + Namespace: "default", + ServiceName: "nginx", + Port: 80, + } + + result := key.Key() + assert.Equal(t, "k8s/80/nginx/default/c123", result) + assert.Equal(t, DEFAULT_PREFIX, key.Prefix) // Should set default prefix +} + +func TestLoadListenerNamedKey(t *testing.T) { + tests := []struct { + name string + key string + want *ListenerNamedKey + wantErr bool + }{ + { + name: "valid key", + key: "k8s/80/nginx/default/c123", + want: &ListenerNamedKey{ + Prefix: DEFAULT_PREFIX, + CID: "c123", + Namespace: "default", + ServiceName: "nginx", + Port: 80, + }, + wantErr: false, + }, + { + name: "invalid prefix", + key: "custom/80/nginx/default/c123", + want: nil, + wantErr: true, + }, + { + name: "invalid format - too few parts", + key: "k8s/80/nginx", + want: nil, + wantErr: true, + }, + { + name: "invalid port", + key: "k8s/abc/nginx/default/c123", + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := LoadListenerNamedKey(tt.key) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestVGroupNamedKey_String(t *testing.T) { + tests := []struct { + name string + key *VGroupNamedKey + want string + }{ + { + name: "nil key", + key: nil, + want: "", + }, + { + name: "valid key with default prefix", + key: &VGroupNamedKey{ + CID: "c123", + Namespace: "default", + ServiceName: "nginx", + VGroupPort: "80", + }, + want: "k8s/80/nginx/default/c123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.key.String()) + }) + } +} + +func TestVGroupNamedKey_Key(t *testing.T) { + key := &VGroupNamedKey{ + CID: "c123", + Namespace: "default", + ServiceName: "nginx", + VGroupPort: "80", + } + + result := key.Key() + assert.Equal(t, "k8s/80/nginx/default/c123", result) + assert.Equal(t, DEFAULT_PREFIX, key.Prefix) +} + +func TestLoadVGroupNamedKey(t *testing.T) { + tests := []struct { + name string + key string + want *VGroupNamedKey + wantErr bool + }{ + { + name: "valid key", + key: "k8s/80/nginx/default/c123", + want: &VGroupNamedKey{ + Prefix: DEFAULT_PREFIX, + CID: "c123", + Namespace: "default", + ServiceName: "nginx", + VGroupPort: "80", + }, + wantErr: false, + }, + { + name: "invalid prefix", + key: "custom/80/nginx/default/c123", + want: nil, + wantErr: true, + }, + { + name: "invalid format - too few parts", + key: "k8s/80/nginx", + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := LoadVGroupNamedKey(tt.key) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestFormatError_Error(t *testing.T) { + err := formatError{key: "invalid/key"} + result := err.Error() + assert.Contains(t, result, FORMAT_ERROR) + assert.Contains(t, result, "invalid/key") +} diff --git a/pkg/model/nlb/nlb.go b/pkg/model/nlb/nlb.go index a248599d4..de66e5318 100644 --- a/pkg/model/nlb/nlb.go +++ b/pkg/model/nlb/nlb.go @@ -113,7 +113,7 @@ type NetworkLoadBalancer struct { } func (l *NetworkLoadBalancer) GetLoadBalancerId() string { - if l == nil { + if l == nil || l.LoadBalancerAttribute == nil { return "" } return l.LoadBalancerAttribute.LoadBalancerId diff --git a/pkg/model/nlb/nlb_test.go b/pkg/model/nlb/nlb_test.go new file mode 100644 index 000000000..1ce725db5 --- /dev/null +++ b/pkg/model/nlb/nlb_test.go @@ -0,0 +1,413 @@ +package nlb + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetAddressType(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "Internet address type", + input: "Internet", + expected: InternetAddressType, + }, + { + name: "internet address type lowercase", + input: "internet", + expected: InternetAddressType, + }, + { + name: "Intranet address type", + input: "Intranet", + expected: IntranetAddressType, + }, + { + name: "intranet address type lowercase", + input: "intranet", + expected: IntranetAddressType, + }, + { + name: "unknown address type", + input: "unknown", + expected: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetAddressType(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestGetAddressIpVersion(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "IPv4 version", + input: "ipv4", + expected: IPv4, + }, + { + name: "IPv4 version uppercase", + input: "IPv4", + expected: IPv4, + }, + { + name: "DualStack version", + input: "DualStack", + expected: DualStack, + }, + { + name: "dualstack version lowercase", + input: "dualstack", + expected: DualStack, + }, + { + name: "unknown version", + input: "unknown", + expected: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetAddressIpVersion(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestGetListenerProtocolType(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "TCP protocol", + input: "TCP", + expected: TCP, + }, + { + name: "tcp protocol lowercase", + input: "tcp", + expected: TCP, + }, + { + name: "UDP protocol", + input: "UDP", + expected: UDP, + }, + { + name: "udp protocol lowercase", + input: "udp", + expected: UDP, + }, + { + name: "TCPSSL protocol", + input: "TCPSSL", + expected: TCPSSL, + }, + { + name: "tcpssl protocol lowercase", + input: "tcpssl", + expected: TCPSSL, + }, + { + name: "unknown protocol", + input: "unknown", + expected: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetListenerProtocolType(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestNetworkLoadBalancer_GetLoadBalancerId(t *testing.T) { + // Test normal case + lb := &NetworkLoadBalancer{ + LoadBalancerAttribute: &LoadBalancerAttribute{ + LoadBalancerId: "lb-12345", + }, + } + assert.Equal(t, "lb-12345", lb.GetLoadBalancerId()) + + // Test nil LoadBalancerAttribute + lbNil := &NetworkLoadBalancer{} + assert.Equal(t, "", lbNil.GetLoadBalancerId()) + + // Test nil NetworkLoadBalancer + var nilLb *NetworkLoadBalancer + assert.Equal(t, "", nilLb.GetLoadBalancerId()) +} + +func TestListenerAttribute_PortString(t *testing.T) { + // Test with ListenerPort + listener1 := &ListenerAttribute{ + ListenerPort: 80, + } + assert.Equal(t, "80", listener1.PortString()) + + // Test with StartPort and EndPort + listener2 := &ListenerAttribute{ + StartPort: 1000, + EndPort: 2000, + } + assert.Equal(t, "1000-2000", listener2.PortString()) + + // Test with all ports zero + listener3 := &ListenerAttribute{} + assert.Equal(t, "0-0", listener3.PortString()) +} + +func TestServerGroup_BackendInfo(t *testing.T) { + // Test with few servers + servers := make([]ServerGroupServer, 2) + servers[0] = ServerGroupServer{ + ServerId: "server-1", + Port: 80, + } + servers[1] = ServerGroupServer{ + ServerId: "server-2", + Port: 8080, + } + + sg := &ServerGroup{ + Servers: servers, + } + + jsonData, _ := json.Marshal(servers) + expected := string(jsonData) + assert.Equal(t, expected, sg.BackendInfo()) + + // Test with many servers (more than 100) + manyServers := make([]ServerGroupServer, 150) + for i := 0; i < 150; i++ { + manyServers[i] = ServerGroupServer{ + ServerId: "server-" + string(rune(i)), + Port: int32(i), + } + } + + sgMany := &ServerGroup{ + Servers: manyServers, + } + + // Should only include first 100 + truncatedServers := manyServers[:100] + jsonData, _ = json.Marshal(truncatedServers) + expected = string(jsonData) + assert.Equal(t, expected, sgMany.BackendInfo()) +} + +func TestNamedKey_IsManagedByService(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "test-namespace", + }, + } + + key := &NamedKey{ + CID: "cluster-id", + Namespace: "test-namespace", + ServiceName: "test-service", + } + + // Test positive case + assert.True(t, key.IsManagedByService(svc, "cluster-id")) + + // Test wrong cluster id + assert.False(t, key.IsManagedByService(svc, "wrong-cluster-id")) + + // Test wrong namespace + key.Namespace = "wrong-namespace" + assert.False(t, key.IsManagedByService(svc, "cluster-id")) + + // Test wrong service name + key.Namespace = "test-namespace" + key.ServiceName = "wrong-service" + assert.False(t, key.IsManagedByService(svc, "cluster-id")) + + // Test nil key + var nilKey *NamedKey + assert.False(t, nilKey.IsManagedByService(svc, "cluster-id")) +} + +func TestListenerNamedKey_String(t *testing.T) { + // Test normal case + key := &ListenerNamedKey{ + NamedKey: NamedKey{ + Prefix: "k8s", + CID: "cluster-id", + Namespace: "test-namespace", + ServiceName: "test-service", + }, + Port: 80, + Protocol: "TCP", + } + + expected := "k8s.80.TCP.test-service.test-namespace.cluster-id" + assert.Equal(t, expected, key.String()) + assert.Equal(t, expected, key.Key()) + + // Test with range ports + keyRange := &ListenerNamedKey{ + NamedKey: NamedKey{ + Prefix: "k8s", + CID: "cluster-id", + Namespace: "test-namespace", + ServiceName: "test-service", + }, + StartPort: 1000, + EndPort: 2000, + Protocol: "TCP", + } + + expectedRange := "k8s.1000_2000.TCP.test-service.test-namespace.cluster-id" + assert.Equal(t, expectedRange, keyRange.String()) + assert.Equal(t, expectedRange, keyRange.Key()) + + // Test nil key + var nilKey *ListenerNamedKey + assert.Equal(t, "", nilKey.String()) +} + +func TestLoadNLBListenerNamedKey(t *testing.T) { + // Test valid key with single port + validKey := "k8s.80.TCP.test-service.test-namespace.cluster-id" + result, err := LoadNLBListenerNamedKey(validKey) + assert.NoError(t, err) + assert.Equal(t, int32(80), result.Port) + assert.Equal(t, "TCP", result.Protocol) + assert.Equal(t, "test-service", result.ServiceName) + assert.Equal(t, "test-namespace", result.Namespace) + assert.Equal(t, "cluster-id", result.CID) + + // Test valid key with port range + validRangeKey := "k8s.1000_2000.TCP.test-service.test-namespace.cluster-id" + resultRange, err := LoadNLBListenerNamedKey(validRangeKey) + assert.NoError(t, err) + assert.Equal(t, int32(1000), resultRange.StartPort) + assert.Equal(t, int32(2000), resultRange.EndPort) + assert.Equal(t, "TCP", resultRange.Protocol) + assert.Equal(t, "test-service", resultRange.ServiceName) + assert.Equal(t, "test-namespace", resultRange.Namespace) + assert.Equal(t, "cluster-id", resultRange.CID) + + // Test invalid key format + invalidKey := "invalid-key-format" + _, err = LoadNLBListenerNamedKey(invalidKey) + assert.Error(t, err) + + // Test invalid port range format + invalidPortKey := "k8s.invalid_port.TCP.test-service.test-namespace.cluster-id" + _, err = LoadNLBListenerNamedKey(invalidPortKey) + assert.Error(t, err) +} + +func TestSGNamedKey_String(t *testing.T) { + // Test normal case + key := &SGNamedKey{ + NamedKey: NamedKey{ + Prefix: "k8s", + CID: "cluster-id", + Namespace: "test-namespace", + ServiceName: "test-service", + }, + Protocol: "TCP", + SGGroupPort: "80", + } + + expected := "k8s.80.TCP.test-service.test-namespace.cluster-id" + assert.Equal(t, expected, key.String()) + assert.Equal(t, expected, key.Key()) + + // Test nil key + var nilKey *SGNamedKey + assert.Equal(t, "", nilKey.String()) +} + +func TestSGNamedKey_Key_EmptyPrefix(t *testing.T) { + key := &SGNamedKey{ + NamedKey: NamedKey{ + Prefix: "", + CID: "cluster-id", + Namespace: "test-namespace", + ServiceName: "test-service", + }, + Protocol: "TCP", + SGGroupPort: "80", + } + expected := "k8s.80.TCP.test-service.test-namespace.cluster-id" + assert.Equal(t, expected, key.Key()) + assert.Equal(t, "k8s", key.Prefix) +} + +func TestLoadNLBSGNamedKey(t *testing.T) { + // Test valid key + validKey := "k8s.80.TCP.test-service.test-namespace.cluster-id" + result, err := LoadNLBSGNamedKey(validKey) + assert.NoError(t, err) + assert.Equal(t, "80", result.SGGroupPort) + assert.Equal(t, "TCP", result.Protocol) + assert.Equal(t, "test-service", result.ServiceName) + assert.Equal(t, "test-namespace", result.Namespace) + assert.Equal(t, "cluster-id", result.CID) + + // Test invalid key format + invalidKey := "invalid-key-format" + _, err = LoadNLBSGNamedKey(invalidKey) + assert.Error(t, err) +} + +func TestParseListenerPortKey(t *testing.T) { + // Test single port + port, startPort, endPort, err := parseListenerPortKey("80") + assert.NoError(t, err) + assert.Equal(t, int32(80), port) + assert.Equal(t, int32(0), startPort) + assert.Equal(t, int32(0), endPort) + + // Test port range + port, startPort, endPort, err = parseListenerPortKey("1000_2000") + assert.NoError(t, err) + assert.Equal(t, int32(0), port) + assert.Equal(t, int32(1000), startPort) + assert.Equal(t, int32(2000), endPort) + + // Test invalid format + _, _, _, err = parseListenerPortKey("invalid") + assert.Error(t, err) + + // Test invalid range format + _, _, _, err = parseListenerPortKey("1000_2000_3000") + assert.Error(t, err) + + // Test invalid start port + _, _, _, err = parseListenerPortKey("invalid_2000") + assert.Error(t, err) + + // Test invalid end port + _, _, _, err = parseListenerPortKey("1000_invalid") + assert.Error(t, err) +} diff --git a/pkg/provider/vmock/ecs.go b/pkg/provider/vmock/ecs.go index 3ececb591..bc978af20 100644 --- a/pkg/provider/vmock/ecs.go +++ b/pkg/provider/vmock/ecs.go @@ -2,13 +2,16 @@ package vmock import ( "context" + "fmt" + "strings" v1 "k8s.io/api/core/v1" "k8s.io/cloud-provider-alibaba-cloud/pkg/model" ecsmodel "k8s.io/cloud-provider-alibaba-cloud/pkg/model/ecs" "k8s.io/cloud-provider-alibaba-cloud/pkg/model/tag" - "k8s.io/cloud-provider-alibaba-cloud/pkg/provider" + prvd "k8s.io/cloud-provider-alibaba-cloud/pkg/provider" "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/alibaba/base" + "k8s.io/cloud-provider-alibaba-cloud/pkg/provider/alibaba/util" ) func NewMockECS( @@ -36,10 +39,22 @@ const ( ) func (d *MockECS) ListInstances(ctx context.Context, ids []string) (map[string]*prvd.NodeAttribute, error) { + for _, id := range ids { + if strings.Contains(id, "list-instances-error") { + return nil, fmt.Errorf("mock list instances error") + } + } mins := make(map[string]*prvd.NodeAttribute) for _, id := range ids { + if strings.Contains(id, "not-exists") { + continue + } + _, node, err := util.NodeFromProviderID(id) + if err != nil { + return nil, err + } mins[id] = &prvd.NodeAttribute{ - InstanceID: id, + InstanceID: node, InstanceType: InstanceType, Addresses: []v1.NodeAddress{ { @@ -54,6 +69,7 @@ func (d *MockECS) ListInstances(ctx context.Context, ids []string) (map[string]* Tags: map[string]string{ tagKeyNodePoolID: NodePoolID, }, + PrimaryNetworkInterfaceID: fmt.Sprintf("eni-%s", node), } } return mins, nil @@ -64,6 +80,9 @@ func (d *MockECS) GetInstancesByIP(ctx context.Context, ips []string) (*prvd.Nod } func (d *MockECS) DescribeNetworkInterfaces(vpcId string, ips []string, ipVersionType model.AddressIPVersionType) (map[string]string, error) { + if vpcId == "vpc-describe-eni-error" { + return nil, fmt.Errorf("mock DescribeNetworkInterfaces error") + } eniids := make(map[string]string) for _, ip := range ips { eniids[ip] = "eni-id" @@ -85,6 +104,9 @@ func (d *MockECS) DescribeNetworkInterfacesByIDs(ids []string) ([]*prvd.EniAttri } func (d *MockECS) ModifyNetworkInterfaceSourceDestCheck(id string, enabled bool) error { + if strings.Contains(id, "error") { + return fmt.Errorf("error") + } return nil } diff --git a/pkg/provider/vmock/nlb.go b/pkg/provider/vmock/nlb.go index 2b8908dc9..8f9179a70 100644 --- a/pkg/provider/vmock/nlb.go +++ b/pkg/provider/vmock/nlb.go @@ -2,6 +2,7 @@ package vmock import ( "context" + "fmt" "time" "github.com/alibabacloud-go/tea/tea" @@ -262,7 +263,6 @@ func (m MockNLB) ListNLBServerGroups(ctx context.Context, tags []tag.Tag) ([]*nl } func (m MockNLB) GetNLBServerGroup(ctx context.Context, sgId string) (*nlbmodel.ServerGroup, error) { - // fixme: fixme if sgId == "rsp-tcpssl-443" { return &nlbmodel.ServerGroup{ ServerGroupId: "rsp-tcpssl-443", @@ -282,6 +282,40 @@ func (m MockNLB) GetNLBServerGroup(ctx context.Context, sgId string) (*nlbmodel. }, }, nil } + if sgId == "sg-user-managed-id" { + return &nlbmodel.ServerGroup{ + ServerGroupId: "sg-user-managed-id", + IsUserManaged: true, + Servers: []nlbmodel.ServerGroupServer{ + { + ServerId: "ecs-id-1", + ServerType: "Ecs", + Port: 80, + Weight: 100, + }, + }, + }, nil + } + if sgId == "sg-not-user-managed-id" { + return &nlbmodel.ServerGroup{ + ServerGroupId: "sg-not-user-managed-id", + IsUserManaged: false, + Servers: []nlbmodel.ServerGroupServer{ + { + ServerId: "ecs-id-1", + ServerType: "Ecs", + Port: 80, + Weight: 100, + }, + }, + }, nil + } + if sgId == "sg-error-id" { + return nil, fmt.Errorf("mock error: cannot find server group") + } + if sgId == "sg-not-found-id" { + return nil, nil + } return nil, nil } diff --git a/pkg/provider/vmock/slb.go b/pkg/provider/vmock/slb.go index feb102187..90dc25062 100644 --- a/pkg/provider/vmock/slb.go +++ b/pkg/provider/vmock/slb.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "strings" + "time" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -455,7 +457,24 @@ func (m *MockCLB) ModifyVServerGroupBackendServers(ctx context.Context, vGroupId } func (m *MockCLB) DescribeServerCertificateById(ctx context.Context, serverCertificateId string) (*model.CertAttribute, error) { - return nil, nil + if strings.Contains(serverCertificateId, "not-found") { + return nil, nil + } + + if strings.Contains(serverCertificateId, "expired") { + return &model.CertAttribute{ + ServerCertificateId: serverCertificateId, + CreateTimeStamp: time.Now().Add(-48 * time.Hour).UnixMilli(), + ExpireTimeStamp: time.Now().Add(-24 * time.Hour).UnixMilli(), + CommonName: "*.example.com", + }, nil + } + return &model.CertAttribute{ + ServerCertificateId: serverCertificateId, + CreateTimeStamp: time.Now().Add(-24 * time.Hour).UnixMilli(), + ExpireTimeStamp: time.Now().Add(24 * time.Hour).UnixMilli(), + CommonName: "*.example.com", + }, nil } func (m *MockCLB) DescribeDomainExtensions(ctx context.Context, lbId string, port int) ([]model.DomainExtension, error) { diff --git a/pkg/provider/vmock/vpc.go b/pkg/provider/vmock/vpc.go index 0470e6403..50b34254b 100644 --- a/pkg/provider/vmock/vpc.go +++ b/pkg/provider/vmock/vpc.go @@ -2,6 +2,7 @@ package vmock import ( "context" + "fmt" "net" servicesvpc "github.com/aliyun/alibaba-cloud-sdk-go/services/vpc" @@ -24,26 +25,117 @@ type MockVPC struct { } func (m *MockVPC) CreateRoutes(ctx context.Context, table string, routes []*model.Route) ([]string, []prvd.RouteUpdateStatus, error) { - panic("implement me") + var ids []string + var status []prvd.RouteUpdateStatus + for _, r := range routes { + ids = append(ids, fmt.Sprintf("r-%s", r.ProviderId)) + s := prvd.RouteUpdateStatus{Route: r, Failed: false} + if r.NodeReference != nil { + switch r.NodeReference.Name { + case "dup-cidr": + s.Failed = true + s.FailedCode = "VPC_ROUTE_ENTRY_CIDR_BLOCK_DUPLICATE" + s.FailedMessage = "duplicate" + case "status-err": + s.Failed = true + s.FailedCode = "VPC_ROUTE_ENTRY_STATUS_ERROR" + s.FailedMessage = "middle status" + case "create-fail": + s.Failed = true + s.FailedCode = "OTHER" + s.FailedMessage = "create failed" + } + } + status = append(status, s) + } + return ids, status, nil } -func (m *MockVPC) CreateRoute(ctx context.Context, table string, provideID string, destinationCIDR string) (*model.Route, error) { - panic("implement me") +func (m *MockVPC) CreateRoute(ctx context.Context, table string, providerID string, destinationCIDR string) (*model.Route, error) { + if providerID == "i-duplicate" && destinationCIDR == "10.96.0.0/24" { + return nil, fmt.Errorf("InvalidCIDRBlock.Duplicate: cidr already exists") + } + if providerID == "i-dup-find-fail" && destinationCIDR == "10.96.0.0/24" { + return nil, fmt.Errorf("InvalidCIDRBlock.Duplicate: cidr already exists") + } + return &model.Route{ + DestinationCIDR: destinationCIDR, + ProviderId: providerID, + }, nil } func (m *MockVPC) DeleteRoute(ctx context.Context, table, provideID, destinationCIDR string) error { - panic("implement me") + return nil } func (m *MockVPC) DeleteRoutes(ctx context.Context, table string, routes []*model.Route) ([]prvd.RouteUpdateStatus, error) { - panic("implement me") + var status []prvd.RouteUpdateStatus + for _, r := range routes { + s := prvd.RouteUpdateStatus{Route: r, Failed: false} + if r.ProviderId == "i-delete-not-exist" { + s.Failed = true + s.FailedCode = "VPC_ROUTER_ENTRY_NOT_EXIST" + s.FailedMessage = "route not found" + } else if r.ProviderId == "i-delete-fail" { + s.Failed = true + s.FailedCode = "OTHER" + s.FailedMessage = "delete failed" + } + status = append(status, s) + } + return status, nil } func (m *MockVPC) ListRoute(ctx context.Context, table string) ([]*model.Route, error) { - panic("implement me") + if table == "route-table-list-err" { + return nil, fmt.Errorf("list route error") + } + if table == "route-table-invalid-cidr" { + return []*model.Route{ + {DestinationCIDR: "invalid", ProviderId: "i-other", Name: "bad-route"}, + }, nil + } + if table == "route-table-1" { + return []*model.Route{ + {DestinationCIDR: "10.96.0.64/26", ProviderId: "i-other", Name: "conflict-route"}, + }, nil + } + return []*model.Route{}, nil } func (m *MockVPC) FindRoute(ctx context.Context, table, pvid, cidr string) (*model.Route, error) { - panic("implement me") + // Simulate finding a route based on provider ID and CIDR + switch { + case pvid == "i-123" && cidr == "192.168.1.0/24": + return &model.Route{ + Name: "route-1", + DestinationCIDR: "192.168.1.0/24", + ProviderId: "i-123", + }, nil + case pvid == "i-456" && cidr == "": + return &model.Route{ + Name: "route-2", + DestinationCIDR: "192.168.2.0/24", + ProviderId: "i-456", + }, nil + case pvid == "" && cidr == "192.168.1.0/24": + return &model.Route{ + Name: "route-1", + DestinationCIDR: "192.168.1.0/24", + ProviderId: "i-123", + }, nil + case pvid == "" && cidr == "": + return nil, fmt.Errorf("empty query condition") + case pvid == "i-duplicate" && cidr == "10.96.0.0/24": + return &model.Route{ + Name: "route-dup", + DestinationCIDR: "10.96.0.0/24", + ProviderId: "i-duplicate", + }, nil + case pvid == "i-dup-find-fail" && cidr == "10.96.0.0/24": + return nil, nil + } + + return nil, nil } func (m *MockVPC) ListRouteTables(ctx context.Context, vpcID string) ([]string, error) { diff --git a/pkg/util/attempt_test.go b/pkg/util/attempt_test.go new file mode 100644 index 000000000..9bcb21f10 --- /dev/null +++ b/pkg/util/attempt_test.go @@ -0,0 +1,138 @@ +package util + +import ( + "testing" + "time" +) + +func TestAttemptStrategy_Start(t *testing.T) { + strategy := AttemptStrategy{ + Total: 500 * time.Millisecond, + Delay: 100 * time.Millisecond, + Min: 2, + } + + attempt := strategy.Start() + if attempt == nil { + t.Fatal("Expected attempt to be non-nil") + } + + if attempt.count != 0 { + t.Errorf("Expected initial count to be 0, got %d", attempt.count) + } + + if !attempt.force { + t.Error("Expected force to be true initially") + } +} + +func TestAttempt_Next(t *testing.T) { + strategy := AttemptStrategy{ + Total: 100 * time.Millisecond, + Delay: 50 * time.Millisecond, + Min: 2, + } + + attempt := strategy.Start() + + // First call should always return true + if !attempt.Next() { + t.Error("First call to Next() should return true") + } + + // Second call should return true due to Min=2 + if !attempt.Next() { + t.Error("Second call to Next() should return true because of Min=2") + } + + // Third call might return false since we've met the Min requirement + // and exceeded the Total time + result := attempt.Next() + // We don't assert the result here as it depends on timing + t.Logf("Third call to Next() returned: %v", result) +} + +func TestAttempt_HasNext(t *testing.T) { + strategy := AttemptStrategy{ + Total: 100 * time.Millisecond, + Delay: 50 * time.Millisecond, + Min: 1, + } + + attempt := strategy.Start() + + // Initially HasNext should return true because force=true + if !attempt.HasNext() { + t.Error("HasNext() should return true initially") + } + + // After first Next() call + attempt.Next() + + // HasNext should still return true because Min=1 + if !attempt.HasNext() { + t.Error("HasNext() should return true due to Min=1") + } +} + +func TestAttempt_MinRetries(t *testing.T) { + strategy := AttemptStrategy{ + Total: 0, // No time + Delay: 10 * time.Millisecond, + Min: 3, // But require 3 retries + } + + attempt := strategy.Start() + + // Should be able to do at least 3 retries even with 0 total time + for i := 0; i < 3; i++ { + if !attempt.Next() { + t.Errorf("Next() should return true for retry %d due to Min=3", i+1) + } + } +} + +func TestAttempt_TotalTimeLimit(t *testing.T) { + strategy := AttemptStrategy{ + Total: 80 * time.Millisecond, + Delay: 30 * time.Millisecond, + Min: 1, + } + + attempt := strategy.Start() + + // First call should work + if !attempt.Next() { + t.Error("First call to Next() should return true") + } + + // Second call should work due to Min=1 + if !attempt.Next() { + t.Error("Second call to Next() should return true due to Min=1") + } + + result := attempt.Next() + t.Logf("Third call to Next() returned: %v", result) +} + +func TestAttempt_nextSleep(t *testing.T) { + strategy := AttemptStrategy{ + Total: 100 * time.Millisecond, + Delay: 50 * time.Millisecond, + Min: 1, + } + + attempt := strategy.Start() + + now := time.Now() + sleep := attempt.nextSleep(now) + if sleep < 0 || sleep > strategy.Delay { + t.Errorf("Unexpected sleep duration: %v", sleep) + } + + time.Sleep(60 * time.Millisecond) + sleep = attempt.nextSleep(time.Now()) + if sleep != 0 { + t.Errorf("Expected 0 when now is after last+Delay, got %v", sleep) + } +} diff --git a/pkg/util/crd/crd_test.go b/pkg/util/crd/crd_test.go new file mode 100644 index 000000000..4bd5fb260 --- /dev/null +++ b/pkg/util/crd/crd_test.go @@ -0,0 +1,178 @@ +package crd + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery/fake" +) + +func TestConf_getName(t *testing.T) { + conf := &Conf{ + NamePlural: "examples", + Group: "test.example.com", + } + + expected := "examples.test.example.com" + actual := conf.getName() + + assert.Equal(t, expected, actual) +} + +func TestNewClient(t *testing.T) { + fakeClient := apiextfake.NewSimpleClientset() + client := NewClient(fakeClient) + + assert.NotNil(t, client) + assert.Equal(t, fakeClient, client.client) +} + +func TestNewCustomClient(t *testing.T) { + fakeClient := apiextfake.NewSimpleClientset() + client := NewCustomClient(fakeClient) + + assert.NotNil(t, client) + assert.Equal(t, fakeClient, client.client) +} + +func TestAddDefaultCaregories(t *testing.T) { + client := &Client{} + testCases := []struct { + name string + input []string + expectedLength int + }{ + { + name: "Empty categories", + input: []string{}, + expectedLength: 2, // Should add "all" and "kooper" + }, + { + name: "Existing categories without defaults", + input: []string{"custom"}, + expectedLength: 3, // Should add "all" and "kooper" + }, + { + name: "Existing categories with some defaults", + input: []string{"custom", "all"}, + expectedLength: 3, // Should add "kooper" + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := client.addDefaultCaregories(tc.input) + assert.Len(t, result, tc.expectedLength) + }) + } +} + +func TestCreateSubresources(t *testing.T) { + client := &Client{} + + t.Run("Both subresources disabled", func(t *testing.T) { + conf := Conf{ + EnableStatusSubresource: false, + EnableScaleSubresource: nil, + } + result := client.createSubresources(conf) + assert.Nil(t, result) + }) + + t.Run("Status subresrouce enabled", func(t *testing.T) { + conf := Conf{ + EnableStatusSubresource: true, + EnableScaleSubresource: nil, + } + result := client.createSubresources(conf) + assert.NotNil(t, result) + assert.NotNil(t, result.Status) + }) + + t.Run("Scale subresource enabled", func(t *testing.T) { + scale := &apiextv1beta1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + } + conf := Conf{ + EnableStatusSubresource: false, + EnableScaleSubresource: scale, + } + result := client.createSubresources(conf) + assert.NotNil(t, result) + assert.NotNil(t, result.Scale) + }) + + t.Run("Both subresrouces enabled", func(t *testing.T) { + scale := &apiextv1beta1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + } + conf := Conf{ + EnableStatusSubresource: true, + EnableScaleSubresource: scale, + } + result := client.createSubresources(conf) + assert.NotNil(t, result) + assert.NotNil(t, result.Status) + assert.NotNil(t, result.Scale) + }) + +} + +func TestValidateCRD(t *testing.T) { + // Create a fake client that simulates server version response + fakeClient := apiextfake.NewSimpleClientset() + client := &Client{client: fakeClient} + + err := client.validateCRD() + assert.NoError(t, err) +} + +func TestDelete(t *testing.T) { + fakeClient := apiextfake.NewSimpleClientset() + client := &Client{client: fakeClient} + + err := client.Delete("test.crd.com") + assert.Error(t, err) +} + +func TestWaitToBePresent_Timeout(t *testing.T) { + // Create a fake client + fakeClient := apiextfake.NewSimpleClientset() + client := &Client{client: fakeClient} + + // Test timeout scenario - CRD doesn't exist + timeout := 10 * time.Millisecond + err := client.WaitToBePresent("nonexistent.crd.com", timeout) + assert.Error(t, err) + assert.Contains(t, err.Error(), "timeout waiting for CRD") +} + +func TestEnsurePresent_CreationError(t *testing.T) { + fakeClient := apiextfake.NewSimpleClientset() + client := &Client{client: fakeClient} + fakeDiscovery, ok := fakeClient.Discovery().(*fake.FakeDiscovery) + if !ok { + t.Fatal("Failed to convert fake client to DiscoveryInterface") + } + fakeDiscovery.FakedServerVersion = &version.Info{ + Major: "1", + Minor: "19", + GitVersion: "v1.19.0", + } + + conf := Conf{ + Kind: "TestKind", + NamePlural: "test", + Group: "test.example.com", + Version: "v1", + Scope: NamespaceScoped, + EnableStatusSubresource: false, + } + + err := client.EnsurePresent(conf) + assert.NoError(t, err) +} diff --git a/pkg/util/hash/hash_test.go b/pkg/util/hash/hash_test.go index 97b9bdb36..bd7c9efaf 100644 --- a/pkg/util/hash/hash_test.go +++ b/pkg/util/hash/hash_test.go @@ -1,10 +1,11 @@ package hash import ( + "testing" + "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "testing" ) func TestHashObject(t *testing.T) { @@ -59,9 +60,47 @@ func TestHashLabel(t *testing.T) { } arr := []interface{}{o.Status.Conditions, o.Labels, o.Spec.Unschedulable} - //fmt.Printf("%s\n\n\n",PrettyYaml(arr)) - //fmt.Printf("%s\n\n\n",HashString(arr)) - //fmt.Printf(HashObject(arr)) - assert.Equal(t, HashObject(arr), "03e8cc3ccf0b0b0bff80371bb738d5e8fa755e9da3d022a7617ce612") } + +func TestHashString(t *testing.T) { + o := v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "key": "value", + }, + }, + Spec: v1.NodeSpec{ + Unschedulable: false, + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: "Ready", + Status: "True", + Reason: "OK", + }, + { + Type: "DiskFull", + Reason: "OK", + }, + }, + }, + } + + arr := []interface{}{o.Status.Conditions, o.Labels, o.Spec.Unschedulable} + + expected := "- - reason: OK\n" + + " status: \"True\"\n" + + " type: Ready\n" + + " - reason: OK\n" + + " type: DiskFull\n" + + "- key: value\n" + + assert.Equal(t, HashString(arr), expected) +} + +func TestPrettyYaml_UnsupportedType(t *testing.T) { + result := PrettyYaml(make(chan int)) + assert.Equal(t, "", result) +} diff --git a/pkg/util/metric/metrics_test.go b/pkg/util/metric/metrics_test.go new file mode 100644 index 000000000..36e089729 --- /dev/null +++ b/pkg/util/metric/metrics_test.go @@ -0,0 +1,114 @@ +package metric + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestMsSince(t *testing.T) { + tests := []struct { + name string + duration time.Duration + want float64 + }{ + { + name: "1 second", + duration: 1 * time.Second, + want: 1000, + }, + { + name: "500 milliseconds", + duration: 500 * time.Millisecond, + want: 500, + }, + { + name: "100 milliseconds", + duration: 100 * time.Millisecond, + want: 100, + }, + { + name: "10 milliseconds", + duration: 10 * time.Millisecond, + want: 10, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start := time.Now().Add(-tt.duration) + result := MsSince(start) + // Allow small tolerance for timing variations + assert.InDelta(t, tt.want, result, 50.0) + }) + } +} + +func TestMsSince_Zero(t *testing.T) { + now := time.Now() + result := MsSince(now) + // Should be very close to 0 + assert.InDelta(t, 0.0, result, 10.0) +} + +func TestMsSince_Future(t *testing.T) { + future := time.Now().Add(1 * time.Second) + result := MsSince(future) + // Should be negative + assert.Less(t, result, 0.0) +} + +func TestUniqueServiceCnt(t *testing.T) { + // Understanding the function logic: + // - If uid NOT in map: returns 0 (line 86) + // - If uid IS in map: stores it and returns 1 (lines 88-89) + + t.Run("new UID not in map", func(t *testing.T) { + uid := "test-unique-new" + result := UniqueServiceCnt(uid) + assert.Equal(t, 0.0, result, "first call with new UID should return 0") + }) + + t.Run("existing UID in map", func(t *testing.T) { + uid := "test-unique-existing" + // Manually add to map first to test the other branch + serviceUIDMap.Store(uid, true) + + result := UniqueServiceCnt(uid) + assert.Equal(t, 1.0, result, "call with existing UID should return 1") + }) +} + +func TestRegisterPrometheus(t *testing.T) { + // This test just ensures the function doesn't panic + // We can't easily test the actual registration without setting up a full prometheus registry + assert.NotPanics(t, func() { + RegisterPrometheus() + }) +} + +func TestMetricVariables(t *testing.T) { + // Test that metric variables are not nil + assert.NotNil(t, NodeLatency) + assert.NotNil(t, RouteLatency) + assert.NotNil(t, SLBLatency) + assert.NotNil(t, SLBOperationStatus) +} + +func TestSLBTypeConstants(t *testing.T) { + assert.Equal(t, "CLBType", CLBType) + assert.Equal(t, "NLBType", NLBType) + assert.Equal(t, "ALBType", ALBType) +} + +func TestVerbConstants(t *testing.T) { + assert.Equal(t, "Creation", string(VerbCreation)) + assert.Equal(t, "Deletion", string(VerbDeletion)) + assert.Equal(t, "Update", string(VerbUpdate)) +} + +func TestOperationResultConstants(t *testing.T) { + assert.Equal(t, "Fail", string(ResultFail)) + assert.Equal(t, "Success", string(ResultSuccess)) +} diff --git a/pkg/util/reconcile_test.go b/pkg/util/reconcile_test.go new file mode 100644 index 000000000..0cb3eac67 --- /dev/null +++ b/pkg/util/reconcile_test.go @@ -0,0 +1,53 @@ +package util + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestNewReconcileNeedRequeue(t *testing.T) { + reason := "test reason" + err := NewReconcileNeedRequeue(reason) + assert.Equal(t, err.reason, reason) + assert.Equal(t, err.Error(), reason) +} + +func TestHandleReconcileResult(t *testing.T) { + t.Run("need requeue", func(t *testing.T) { + originalErr := NewReconcileNeedRequeue("test reason") + r, err := HandleReconcileResult(reconcile.Request{}, originalErr) + assert.NoError(t, err) + assert.Zero(t, r.Requeue) + assert.Equal(t, r.RequeueAfter, defaultRequeueAfter) + }) + + t.Run("need requeue with retry after", func(t *testing.T) { + after := time.Second * 200 + assert.NotEqual(t, after, defaultRequeueAfter) + originalErr := NewReconcileNeedRequeue("test reason") + originalErr.after = after + r, err := HandleReconcileResult(reconcile.Request{}, originalErr) + assert.NoError(t, err) + assert.Zero(t, r.Requeue) + assert.Equal(t, r.RequeueAfter, after) + }) + + t.Run("nil error", func(t *testing.T) { + r, err := HandleReconcileResult(reconcile.Request{}, nil) + assert.NoError(t, err) + assert.Zero(t, r.Requeue) + assert.Zero(t, r.RequeueAfter) + }) + + t.Run("normal error", func(t *testing.T) { + originalErr := errors.New("test error") + r, err := HandleReconcileResult(reconcile.Request{}, originalErr) + assert.Error(t, err) + assert.Zero(t, r.Requeue) + assert.Zero(t, r.RequeueAfter) + }) +} diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 0858f9ece..a75529d7e 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -35,7 +35,7 @@ func PrettyJson(object interface{}) string { } // ClusterVersionAtLeast Check kubernetes version whether higher than the specific version -func ClusterVersionAtLeast(client *apiext.Clientset, min string) (bool, error) { +func ClusterVersionAtLeast(client apiext.Interface, min string) (bool, error) { serverVersion, err := client.Discovery().ServerVersion() if err != nil { return false, fmt.Errorf("get server version: %s", err.Error()) @@ -50,6 +50,7 @@ func ClusterVersionAtLeast(client *apiext.Clientset, min string) (bool, error) { least, err := version.ParseGeneric(min) if err != nil { klog.Errorf("parse version %s error: %s", min, err.Error()) + return false, fmt.Errorf("parse version %s error: %s", min, err.Error()) } return runningVersion.AtLeast(least), nil diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go new file mode 100644 index 000000000..29cf4039c --- /dev/null +++ b/pkg/util/utils_test.go @@ -0,0 +1,207 @@ +package util + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + apiextfakes "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/version" + discoveryfake "k8s.io/client-go/discovery/fake" +) + +type testObject struct { + metav1.ObjectMeta +} + +func TestNamespacedName(t *testing.T) { + obj := &testObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-name", + }, + } + + namespacedName := NamespacedName(obj) + assert.Equal(t, "test-namespace", namespacedName.Namespace) + assert.Equal(t, "test-name", namespacedName.Name) +} + +func TestKey(t *testing.T) { + obj := &testObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-name", + }, + } + + key := Key(obj) + assert.Equal(t, "test-namespace/test-name", key) +} + +func TestPrettyJson(t *testing.T) { + obj := struct { + Name string `json:"name"` + Age int `json:"age"` + }{ + Name: "test-user", + Age: 25, + } + + result := PrettyJson(obj) + expected := `{ + "name": "test-user", + "age": 25 +}` + assert.Equal(t, expected, result) + + invalidObj := make(chan int) + result = PrettyJson(invalidObj) + assert.Equal(t, "", result) +} + +func TestMergeStringMap(t *testing.T) { + // Test basic merge + map1 := map[string]string{"a": "1", "b": "2"} + map2 := map[string]string{"c": "3", "d": "4"} + result := MergeStringMap(map1, map2) + expected := map[string]string{"a": "1", "b": "2", "c": "3", "d": "4"} + assert.Equal(t, expected, result) + + // Test with overlapping keys (first value should be kept) + map3 := map[string]string{"a": "1", "b": "2"} + map4 := map[string]string{"a": "3", "d": "4"} + result = MergeStringMap(map3, map4) + expected = map[string]string{"a": "1", "b": "2", "d": "4"} + assert.Equal(t, expected, result) + + // Test with empty maps + empty := map[string]string{} + result = MergeStringMap(empty, map1) + assert.Equal(t, map1, result) + + result = MergeStringMap(map1, empty) + assert.Equal(t, map1, result) + + // Test with multiple maps + map5 := map[string]string{"e": "5"} + result = MergeStringMap(map1, map2, map5) + expected = map[string]string{"a": "1", "b": "2", "c": "3", "d": "4", "e": "5"} + assert.Equal(t, expected, result) +} + +func TestIsStringSliceEqual(t *testing.T) { + // Test equal slices + s1 := []string{"a", "b", "c"} + s2 := []string{"a", "b", "c"} + assert.True(t, IsStringSliceEqual(s1, s2)) + + // Test equal slices with different order + s3 := []string{"c", "b", "a"} + assert.True(t, IsStringSliceEqual(s1, s3)) + + // Test equal slices with case differences + s4 := []string{"A", "B", "C"} + assert.True(t, IsStringSliceEqual(s1, s4)) + + // Test different length slices + s5 := []string{"a", "b"} + assert.False(t, IsStringSliceEqual(s1, s5)) + + // Test different content + s6 := []string{"a", "b", "d"} + assert.False(t, IsStringSliceEqual(s1, s6)) + + // Test empty slices + s7 := []string{} + s8 := []string{} + assert.True(t, IsStringSliceEqual(s7, s8)) + + // Test one empty slice + assert.False(t, IsStringSliceEqual(s1, s7)) +} + +func TestClusterVersionAtLeast(t *testing.T) { + client := apiextfakes.NewSimpleClientset() + fakeDiscovery, ok := client.Discovery().(*discoveryfake.FakeDiscovery) + if !ok { + t.Fatal("failed to create fake discovery client") + } + fakeDiscovery.FakedServerVersion = &version.Info{ + Major: "1", + Minor: "20", + GitVersion: "v1.20.0", + } + + ok, err := ClusterVersionAtLeast(client, "v1.18.0") + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = ClusterVersionAtLeast(client, "v1.22.0") + assert.NoError(t, err) + assert.False(t, ok) + + _, err = ClusterVersionAtLeast(client, "invalid") + assert.Error(t, err) + + fakeDiscovery.FakedServerVersion = &version.Info{ + GitVersion: "invalid", + } + _, err = ClusterVersionAtLeast(client, "v1.18.0") + assert.Error(t, err) + assert.Contains(t, err.Error(), "unexpected error parsing running Kubernetes version") +} + +func TestRetryImmediateOnError(t *testing.T) { + // Test successful function on first try + var attempts int + fn := func() error { + attempts++ + return nil + } + + err := RetryImmediateOnError(time.Millisecond, time.Millisecond*10, nil, fn) + assert.NoError(t, err) + assert.Equal(t, 1, attempts) + + // Test retryable error that eventually succeeds + attempts = 0 + var shouldFail = true + fn = func() error { + attempts++ + if shouldFail && attempts < 3 { + return &testError{"retryable error"} + } + return nil + } + + retryable := func(err error) bool { + return strings.Contains(err.Error(), "retryable") + } + + err = RetryImmediateOnError(time.Millisecond, time.Millisecond*50, retryable, fn) + assert.NoError(t, err) + assert.Equal(t, 3, attempts) + + // Test non-retryable error + attempts = 0 + fn = func() error { + attempts++ + return &testError{"normal error"} + } + + err = RetryImmediateOnError(time.Millisecond, time.Millisecond*50, retryable, fn) + assert.Error(t, err) + assert.Equal(t, 1, attempts) + assert.Contains(t, err.Error(), "normal error") +} + +type testError struct { + msg string +} + +func (e *testError) Error() string { + return e.msg +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go new file mode 100644 index 000000000..325781f5f --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/clientset_generated.go @@ -0,0 +1,92 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + fakeapiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" + fakeapiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) + +// ApiextensionsV1 retrieves the ApiextensionsV1Client +func (c *Clientset) ApiextensionsV1() apiextensionsv1.ApiextensionsV1Interface { + return &fakeapiextensionsv1.FakeApiextensionsV1{Fake: &c.Fake} +} + +// ApiextensionsV1beta1 retrieves the ApiextensionsV1beta1Client +func (c *Clientset) ApiextensionsV1beta1() apiextensionsv1beta1.ApiextensionsV1beta1Interface { + return &fakeapiextensionsv1beta1.FakeApiextensionsV1beta1{Fake: &c.Fake} +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go new file mode 100644 index 000000000..9b99e7167 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go new file mode 100644 index 000000000..355c69907 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake/register.go @@ -0,0 +1,58 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + apiextensionsv1.AddToScheme, + apiextensionsv1beta1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go new file mode 100644 index 000000000..16f443990 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go new file mode 100644 index 000000000..43b4ee7a2 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_apiextensions_client.go @@ -0,0 +1,40 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeApiextensionsV1 struct { + *testing.Fake +} + +func (c *FakeApiextensionsV1) CustomResourceDefinitions() v1.CustomResourceDefinitionInterface { + return &FakeCustomResourceDefinitions{c} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeApiextensionsV1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go new file mode 100644 index 000000000..9402e0569 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake/fake_customresourcedefinition.go @@ -0,0 +1,178 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/applyconfiguration/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeCustomResourceDefinitions implements CustomResourceDefinitionInterface +type FakeCustomResourceDefinitions struct { + Fake *FakeApiextensionsV1 +} + +var customresourcedefinitionsResource = v1.SchemeGroupVersion.WithResource("customresourcedefinitions") + +var customresourcedefinitionsKind = v1.SchemeGroupVersion.WithKind("CustomResourceDefinition") + +// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any. +func (c *FakeCustomResourceDefinitions) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.CustomResourceDefinition, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(customresourcedefinitionsResource, name), &v1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1.CustomResourceDefinition), err +} + +// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors. +func (c *FakeCustomResourceDefinitions) List(ctx context.Context, opts metav1.ListOptions) (result *v1.CustomResourceDefinitionList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(customresourcedefinitionsResource, customresourcedefinitionsKind, opts), &v1.CustomResourceDefinitionList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1.CustomResourceDefinitionList{ListMeta: obj.(*v1.CustomResourceDefinitionList).ListMeta} + for _, item := range obj.(*v1.CustomResourceDefinitionList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested customResourceDefinitions. +func (c *FakeCustomResourceDefinitions) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(customresourcedefinitionsResource, opts)) +} + +// Create takes the representation of a customResourceDefinition and creates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any. +func (c *FakeCustomResourceDefinitions) Create(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.CreateOptions) (result *v1.CustomResourceDefinition, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(customresourcedefinitionsResource, customResourceDefinition), &v1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1.CustomResourceDefinition), err +} + +// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any. +func (c *FakeCustomResourceDefinitions) Update(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.UpdateOptions) (result *v1.CustomResourceDefinition, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(customresourcedefinitionsResource, customResourceDefinition), &v1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1.CustomResourceDefinition), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeCustomResourceDefinitions) UpdateStatus(ctx context.Context, customResourceDefinition *v1.CustomResourceDefinition, opts metav1.UpdateOptions) (*v1.CustomResourceDefinition, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(customresourcedefinitionsResource, "status", customResourceDefinition), &v1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1.CustomResourceDefinition), err +} + +// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs. +func (c *FakeCustomResourceDefinitions) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(customresourcedefinitionsResource, name, opts), &v1.CustomResourceDefinition{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCustomResourceDefinitions) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(customresourcedefinitionsResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1.CustomResourceDefinitionList{}) + return err +} + +// Patch applies the patch and returns the patched customResourceDefinition. +func (c *FakeCustomResourceDefinitions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.CustomResourceDefinition, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, name, pt, data, subresources...), &v1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1.CustomResourceDefinition), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied customResourceDefinition. +func (c *FakeCustomResourceDefinitions) Apply(ctx context.Context, customResourceDefinition *apiextensionsv1.CustomResourceDefinitionApplyConfiguration, opts metav1.ApplyOptions) (result *v1.CustomResourceDefinition, err error) { + if customResourceDefinition == nil { + return nil, fmt.Errorf("customResourceDefinition provided to Apply must not be nil") + } + data, err := json.Marshal(customResourceDefinition) + if err != nil { + return nil, err + } + name := customResourceDefinition.Name + if name == nil { + return nil, fmt.Errorf("customResourceDefinition.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, *name, types.ApplyPatchType, data), &v1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1.CustomResourceDefinition), err +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *FakeCustomResourceDefinitions) ApplyStatus(ctx context.Context, customResourceDefinition *apiextensionsv1.CustomResourceDefinitionApplyConfiguration, opts metav1.ApplyOptions) (result *v1.CustomResourceDefinition, err error) { + if customResourceDefinition == nil { + return nil, fmt.Errorf("customResourceDefinition provided to Apply must not be nil") + } + data, err := json.Marshal(customResourceDefinition) + if err != nil { + return nil, err + } + name := customResourceDefinition.Name + if name == nil { + return nil, fmt.Errorf("customResourceDefinition.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, *name, types.ApplyPatchType, data, "status"), &v1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1.CustomResourceDefinition), err +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go new file mode 100644 index 000000000..16f443990 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go new file mode 100644 index 000000000..288683ef9 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_apiextensions_client.go @@ -0,0 +1,40 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeApiextensionsV1beta1 struct { + *testing.Fake +} + +func (c *FakeApiextensionsV1beta1) CustomResourceDefinitions() v1beta1.CustomResourceDefinitionInterface { + return &FakeCustomResourceDefinitions{c} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeApiextensionsV1beta1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go new file mode 100644 index 000000000..250d69a63 --- /dev/null +++ b/vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake/fake_customresourcedefinition.go @@ -0,0 +1,178 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/applyconfiguration/apiextensions/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeCustomResourceDefinitions implements CustomResourceDefinitionInterface +type FakeCustomResourceDefinitions struct { + Fake *FakeApiextensionsV1beta1 +} + +var customresourcedefinitionsResource = v1beta1.SchemeGroupVersion.WithResource("customresourcedefinitions") + +var customresourcedefinitionsKind = v1beta1.SchemeGroupVersion.WithKind("CustomResourceDefinition") + +// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any. +func (c *FakeCustomResourceDefinitions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.CustomResourceDefinition, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(customresourcedefinitionsResource, name), &v1beta1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CustomResourceDefinition), err +} + +// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors. +func (c *FakeCustomResourceDefinitions) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.CustomResourceDefinitionList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(customresourcedefinitionsResource, customresourcedefinitionsKind, opts), &v1beta1.CustomResourceDefinitionList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1beta1.CustomResourceDefinitionList{ListMeta: obj.(*v1beta1.CustomResourceDefinitionList).ListMeta} + for _, item := range obj.(*v1beta1.CustomResourceDefinitionList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested customResourceDefinitions. +func (c *FakeCustomResourceDefinitions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(customresourcedefinitionsResource, opts)) +} + +// Create takes the representation of a customResourceDefinition and creates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any. +func (c *FakeCustomResourceDefinitions) Create(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.CreateOptions) (result *v1beta1.CustomResourceDefinition, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(customresourcedefinitionsResource, customResourceDefinition), &v1beta1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CustomResourceDefinition), err +} + +// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any. +func (c *FakeCustomResourceDefinitions) Update(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.UpdateOptions) (result *v1beta1.CustomResourceDefinition, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(customresourcedefinitionsResource, customResourceDefinition), &v1beta1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CustomResourceDefinition), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeCustomResourceDefinitions) UpdateStatus(ctx context.Context, customResourceDefinition *v1beta1.CustomResourceDefinition, opts v1.UpdateOptions) (*v1beta1.CustomResourceDefinition, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(customresourcedefinitionsResource, "status", customResourceDefinition), &v1beta1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CustomResourceDefinition), err +} + +// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs. +func (c *FakeCustomResourceDefinitions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(customresourcedefinitionsResource, name, opts), &v1beta1.CustomResourceDefinition{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCustomResourceDefinitions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(customresourcedefinitionsResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1beta1.CustomResourceDefinitionList{}) + return err +} + +// Patch applies the patch and returns the patched customResourceDefinition. +func (c *FakeCustomResourceDefinitions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.CustomResourceDefinition, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, name, pt, data, subresources...), &v1beta1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CustomResourceDefinition), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied customResourceDefinition. +func (c *FakeCustomResourceDefinitions) Apply(ctx context.Context, customResourceDefinition *apiextensionsv1beta1.CustomResourceDefinitionApplyConfiguration, opts v1.ApplyOptions) (result *v1beta1.CustomResourceDefinition, err error) { + if customResourceDefinition == nil { + return nil, fmt.Errorf("customResourceDefinition provided to Apply must not be nil") + } + data, err := json.Marshal(customResourceDefinition) + if err != nil { + return nil, err + } + name := customResourceDefinition.Name + if name == nil { + return nil, fmt.Errorf("customResourceDefinition.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, *name, types.ApplyPatchType, data), &v1beta1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CustomResourceDefinition), err +} + +// ApplyStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). +func (c *FakeCustomResourceDefinitions) ApplyStatus(ctx context.Context, customResourceDefinition *apiextensionsv1beta1.CustomResourceDefinitionApplyConfiguration, opts v1.ApplyOptions) (result *v1beta1.CustomResourceDefinition, err error) { + if customResourceDefinition == nil { + return nil, fmt.Errorf("customResourceDefinition provided to Apply must not be nil") + } + data, err := json.Marshal(customResourceDefinition) + if err != nil { + return nil, err + } + name := customResourceDefinition.Name + if name == nil { + return nil, fmt.Errorf("customResourceDefinition.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, *name, types.ApplyPatchType, data, "status"), &v1beta1.CustomResourceDefinition{}) + if obj == nil { + return nil, err + } + return obj.(*v1beta1.CustomResourceDefinition), err +} diff --git a/vendor/k8s.io/client-go/discovery/fake/discovery.go b/vendor/k8s.io/client-go/discovery/fake/discovery.go new file mode 100644 index 000000000..f8a78e1ef --- /dev/null +++ b/vendor/k8s.io/client-go/discovery/fake/discovery.go @@ -0,0 +1,174 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 fake + +import ( + "fmt" + "net/http" + + openapi_v2 "github.com/google/gnostic-models/openapiv2" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/discovery" + "k8s.io/client-go/openapi" + kubeversion "k8s.io/client-go/pkg/version" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/testing" +) + +// FakeDiscovery implements discovery.DiscoveryInterface and sometimes calls testing.Fake.Invoke with an action, +// but doesn't respect the return value if any. There is a way to fake static values like ServerVersion by using the Faked... fields on the struct. +type FakeDiscovery struct { + *testing.Fake + FakedServerVersion *version.Info +} + +// ServerResourcesForGroupVersion returns the supported resources for a group +// and version. +func (c *FakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { + action := testing.ActionImpl{ + Verb: "get", + Resource: schema.GroupVersionResource{Resource: "resource"}, + } + c.Invokes(action, nil) + for _, resourceList := range c.Resources { + if resourceList.GroupVersion == groupVersion { + return resourceList, nil + } + } + return nil, &errors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + Message: fmt.Sprintf("the server could not find the requested resource, GroupVersion %q not found", groupVersion), + }} +} + +// ServerGroupsAndResources returns the supported groups and resources for all groups and versions. +func (c *FakeDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { + sgs, err := c.ServerGroups() + if err != nil { + return nil, nil, err + } + resultGroups := []*metav1.APIGroup{} + for i := range sgs.Groups { + resultGroups = append(resultGroups, &sgs.Groups[i]) + } + + action := testing.ActionImpl{ + Verb: "get", + Resource: schema.GroupVersionResource{Resource: "resource"}, + } + c.Invokes(action, nil) + return resultGroups, c.Resources, nil +} + +// ServerPreferredResources returns the supported resources with the version +// preferred by the server. +func (c *FakeDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) { + return nil, nil +} + +// ServerPreferredNamespacedResources returns the supported namespaced resources +// with the version preferred by the server. +func (c *FakeDiscovery) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { + return nil, nil +} + +// ServerGroups returns the supported groups, with information like supported +// versions and the preferred version. +func (c *FakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) { + action := testing.ActionImpl{ + Verb: "get", + Resource: schema.GroupVersionResource{Resource: "group"}, + } + c.Invokes(action, nil) + + groups := map[string]*metav1.APIGroup{} + + for _, res := range c.Resources { + gv, err := schema.ParseGroupVersion(res.GroupVersion) + if err != nil { + return nil, err + } + group := groups[gv.Group] + if group == nil { + group = &metav1.APIGroup{ + Name: gv.Group, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: res.GroupVersion, + Version: gv.Version, + }, + } + groups[gv.Group] = group + } + + group.Versions = append(group.Versions, metav1.GroupVersionForDiscovery{ + GroupVersion: res.GroupVersion, + Version: gv.Version, + }) + } + + list := &metav1.APIGroupList{} + for _, apiGroup := range groups { + list.Groups = append(list.Groups, *apiGroup) + } + + return list, nil + +} + +// ServerVersion retrieves and parses the server's version. +func (c *FakeDiscovery) ServerVersion() (*version.Info, error) { + action := testing.ActionImpl{} + action.Verb = "get" + action.Resource = schema.GroupVersionResource{Resource: "version"} + _, err := c.Invokes(action, nil) + if err != nil { + return nil, err + } + + if c.FakedServerVersion != nil { + return c.FakedServerVersion, nil + } + + versionInfo := kubeversion.Get() + return &versionInfo, nil +} + +// OpenAPISchema retrieves and parses the swagger API schema the server supports. +func (c *FakeDiscovery) OpenAPISchema() (*openapi_v2.Document, error) { + return &openapi_v2.Document{}, nil +} + +func (c *FakeDiscovery) OpenAPIV3() openapi.Client { + panic("unimplemented") +} + +// RESTClient returns a RESTClient that is used to communicate with API server +// by this client implementation. +func (c *FakeDiscovery) RESTClient() restclient.Interface { + return nil +} + +func (c *FakeDiscovery) WithLegacy() discovery.DiscoveryInterface { + panic("unimplemented") +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8a06fe5b9..d72de91ce 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -431,9 +431,12 @@ k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1 k8s.io/apiextensions-apiserver/pkg/client/applyconfiguration/apiextensions/v1 k8s.io/apiextensions-apiserver/pkg/client/applyconfiguration/apiextensions/v1beta1 k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset +k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1 +k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1/fake k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1 +k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake # k8s.io/apimachinery v0.28.15 ## explicit; go 1.20 k8s.io/apimachinery/pkg/api/equality @@ -541,6 +544,7 @@ k8s.io/client-go/applyconfigurations/storage/v1 k8s.io/client-go/applyconfigurations/storage/v1alpha1 k8s.io/client-go/applyconfigurations/storage/v1beta1 k8s.io/client-go/discovery +k8s.io/client-go/discovery/fake k8s.io/client-go/dynamic k8s.io/client-go/informers k8s.io/client-go/informers/admissionregistration