diff --git a/pkg/aliyun/client/errors/errors_test.go b/pkg/aliyun/client/errors/errors_test.go index 9353ad40..d42cdd2a 100644 --- a/pkg/aliyun/client/errors/errors_test.go +++ b/pkg/aliyun/client/errors/errors_test.go @@ -118,3 +118,10 @@ func TestErrorIs(t *testing.T) { // Test case 3: Check if no check functions are provided assert.False(t, ErrorIs(err)) } + +func TestErrorIsReturnsFalseWhenNoCheckErrMatches(t *testing.T) { + err := apiErr.NewServerError(403, "{\"Code\": \"err\"}", "") + checkFunc1 := WarpFn("anotherErr") + checkFunc2 := WarpFn("yetAnotherErr") + assert.False(t, ErrorIs(err, checkFunc1, checkFunc2)) +} diff --git a/pkg/controller/common/ctx_default.go b/pkg/controller/common/ctx_default.go deleted file mode 100644 index a683fb99..00000000 --- a/pkg/controller/common/ctx_default.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build default_build - -package common - -import ( - "context" - - "github.com/AliyunContainerService/terway/pkg/apis/network.alibabacloud.com/v1beta1" -) - -// WithCtx extract fields from v1beta1.Allocation and set to context.Context -func WithCtx(ctx context.Context, alloc *v1beta1.Allocation) context.Context { - return ctx -} diff --git a/pkg/controller/endpoint/endpoint.go b/pkg/controller/endpoint/endpoint.go deleted file mode 100644 index 5bc47408..00000000 --- a/pkg/controller/endpoint/endpoint.go +++ /dev/null @@ -1,114 +0,0 @@ -package endpoint - -import ( - "context" - "fmt" - "os" - "reflect" - "time" - - register "github.com/AliyunContainerService/terway/pkg/controller" - "github.com/AliyunContainerService/terway/pkg/utils/k8sclient" - - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -var log = ctrl.Log.WithName("endpoint") - -const ControllerName = "endpoint" - -func init() { - register.Add(ControllerName, func(mgr manager.Manager, ctrlCtx *register.ControllerCtx) error { - ipStr := os.Getenv("MY_POD_IP") - if ipStr == "" { - return fmt.Errorf("podIP is not found") - } - // if enable Service name should equal cfg.ControllerName - ep := New(ctrlCtx.Config.ControllerName, ctrlCtx.Config.ControllerNamespace, ipStr, int32(ctrlCtx.Config.WebhookPort)) - return mgr.Add(ep) - }, false) -} - -// ReconcilePodNetworking implements reconcile.Reconciler -var _ manager.Runnable = &Endpoint{} - -// Endpoint reconciles a AutoRepair object -type Endpoint struct { - PodIP string - Name string - Namespace string - Port int32 -} - -func New(name, namespace, podIP string, port int32) *Endpoint { - return &Endpoint{ - PodIP: podIP, - Name: name, - Namespace: namespace, - Port: port, - } -} - -func (m *Endpoint) Start(ctx context.Context) error { - wait.Until(func() { - err := m.RegisterEndpoints() - if err != nil { - log.Error(err, "error sync endpoint") - } - }, time.Minute, ctx.Done()) - return fmt.Errorf("endpoint sync exited") -} - -// NeedLeaderElection need election -func (m *Endpoint) NeedLeaderElection() bool { - return true -} - -// RegisterEndpoints to endpoint -func (m *Endpoint) RegisterEndpoints() error { - newEPSubnet := []v1.EndpointSubset{ - { - Addresses: []v1.EndpointAddress{ - { - IP: m.PodIP, - }, - }, - Ports: []v1.EndpointPort{ - { - Name: "https", - Port: m.Port, - Protocol: "TCP", - }, - }, - }} - ctx := context.Background() - oldEP, err := k8sclient.K8sClient.CoreV1().Endpoints(m.Namespace).Get(ctx, m.Name, metav1.GetOptions{}) - if err != nil { - if !errors.IsNotFound(err) { - return err - } - _, err = k8sclient.K8sClient.CoreV1().Endpoints(m.Namespace).Create(ctx, &v1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: m.Namespace, - Name: m.Name, - }, - Subsets: newEPSubnet, - }, metav1.CreateOptions{}) - log.Info("register endpoint", "ip", m.PodIP) - return err - } - - if reflect.DeepEqual(&oldEP.Subsets, &newEPSubnet) { - return nil - } - copyEP := oldEP.DeepCopy() - copyEP.Subsets = newEPSubnet - _, err = k8sclient.K8sClient.CoreV1().Endpoints(m.Namespace).Update(ctx, copyEP, metav1.UpdateOptions{}) - log.Info("register endpoint", "ip", m.PodIP) - return err -} diff --git a/pkg/controller/pod-eni/eni_controller.go b/pkg/controller/pod-eni/eni_controller.go index b99d3930..b5ca6f31 100644 --- a/pkg/controller/pod-eni/eni_controller.go +++ b/pkg/controller/pod-eni/eni_controller.go @@ -445,7 +445,7 @@ func (m *ReconcilePodENI) gcENIs(ctx context.Context, enis []*aliyunClient.Netwo } now := time.Now() for i, eni := range enis { - if !m.eniFilter(eni, tagFilter) { + if eniFilter(eni, tagFilter) { continue } t, err := time.Parse(layout, eni.CreationTime) @@ -672,7 +672,6 @@ func (m *ReconcilePodENI) attachENI(ctx context.Context, podENI *v1beta1.PodENI) ii := i g.Go(func() error { alloc := podENI.Spec.Allocations[ii] - ctx := common.WithCtx(ctx, &alloc) err := m.aliyun.AttachNetworkInterface(ctx, alloc.ENI.ID, podENI.Status.InstanceID, podENI.Status.TrunkENIID) if err != nil { return err @@ -712,7 +711,6 @@ func (m *ReconcilePodENI) detachMemberENI(ctx context.Context, podENI *v1beta1.P } }() for _, alloc := range podENI.Spec.Allocations { - ctx := common.WithCtx(ctx, &alloc) instanceID := podENI.Status.InstanceID trunkENIID := podENI.Status.TrunkENIID if podENI.Status.InstanceID == "" { @@ -761,7 +759,7 @@ func (m *ReconcilePodENI) deleteMemberENI(ctx context.Context, podENI *v1beta1.P if alloc.ENI.ID == "" { continue } - err = m.aliyun.DeleteNetworkInterface(common.WithCtx(ctx, &alloc), alloc.ENI.ID) + err = m.aliyun.DeleteNetworkInterface(ctx, alloc.ENI.ID) if err != nil { return err } @@ -771,7 +769,7 @@ func (m *ReconcilePodENI) deleteMemberENI(ctx context.Context, podENI *v1beta1.P } // eniFilter will compare eni tags with filter, if all filter match return true -func (m *ReconcilePodENI) eniFilter(eni *aliyunClient.NetworkInterface, filter map[string]string) bool { +func eniFilter(eni *aliyunClient.NetworkInterface, filter map[string]string) bool { for k, v := range filter { found := false for _, tag := range eni.Tags { diff --git a/pkg/controller/pod-eni/eni_controller_test.go b/pkg/controller/pod-eni/eni_controller_test.go new file mode 100644 index 00000000..621955db --- /dev/null +++ b/pkg/controller/pod-eni/eni_controller_test.go @@ -0,0 +1,245 @@ +package podeni + +import ( + "context" + "testing" + + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + aliyunClient "github.com/AliyunContainerService/terway/pkg/aliyun/client" + "github.com/AliyunContainerService/terway/pkg/apis/network.alibabacloud.com/v1beta1" + register "github.com/AliyunContainerService/terway/pkg/controller" + "github.com/AliyunContainerService/terway/pkg/controller/mocks" +) + +func TestReconcilePodENI_podRequirePodENI(t *testing.T) { + type fields struct { + client client.Client + scheme *runtime.Scheme + aliyun register.Interface + record record.EventRecorder + trunkMode bool + crdMode bool + } + type args struct { + ctx context.Context + pod *corev1.Pod + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "normal pod", + fields: fields{ + client: func() client.Client { + nodeReader := fake.NewClientBuilder() + nodeReader.WithObjects(&corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: corev1.NodeSpec{}, + Status: corev1.NodeStatus{}, + }) + return nodeReader.Build() + }(), + scheme: nil, + aliyun: nil, + record: nil, + trunkMode: false, + crdMode: false, + }, + args: args{ + ctx: context.Background(), + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "foo", + }, + Status: corev1.PodStatus{}, + }, + }, + want: false, + }, + { + name: "trunk pod", + fields: fields{ + client: func() client.Client { + nodeReader := fake.NewClientBuilder() + nodeReader.WithObjects(&corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: corev1.NodeSpec{}, + Status: corev1.NodeStatus{}, + }) + return nodeReader.Build() + }(), + scheme: nil, + aliyun: nil, + record: nil, + trunkMode: false, + crdMode: false, + }, + args: args{ + ctx: context.Background(), + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Annotations: map[string]string{ + "k8s.aliyun.com/pod-eni": "true", + }, + }, + Spec: corev1.PodSpec{ + NodeName: "foo", + }, + Status: corev1.PodStatus{}, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &ReconcilePodENI{ + client: tt.fields.client, + scheme: tt.fields.scheme, + aliyun: tt.fields.aliyun, + record: tt.fields.record, + trunkMode: tt.fields.trunkMode, + crdMode: tt.fields.crdMode, + } + if got := m.podRequirePodENI(tt.args.ctx, tt.args.pod); got != tt.want { + t.Errorf("podRequirePodENI() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_eniFilter(t *testing.T) { + type args struct { + eni *aliyunClient.NetworkInterface + filter map[string]string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "match", + args: args{ + eni: &aliyunClient.NetworkInterface{ + Tags: []ecs.Tag{ + { + TagKey: "foo", + TagValue: "bar", + }, + }, + }, + filter: map[string]string{ + "foo": "bar", + }, + }, + want: true, + }, + { + name: "not match", + args: args{ + eni: &aliyunClient.NetworkInterface{ + Tags: []ecs.Tag{ + { + TagKey: "foo", + TagValue: "bar", + }, + }, + }, + filter: map[string]string{ + "foo": "bar", + "foo1": "bar1", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := eniFilter(tt.args.eni, tt.args.filter); got != tt.want { + t.Errorf("eniFilter() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestReconcilePodENI_deleteMemberENI(t *testing.T) { + m := mocks.NewInterface(t) + m.On("DeleteNetworkInterface", mock.Anything, "foo").Return(nil) + + c := &ReconcilePodENI{ + client: fake.NewClientBuilder().Build(), + aliyun: m, + record: record.NewFakeRecorder(10), + } + err := c.deleteMemberENI(context.Background(), &v1beta1.PodENI{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: v1beta1.PodENISpec{ + Allocations: []v1beta1.Allocation{ + { + ENI: v1beta1.ENI{ID: "foo"}, + }, + }, + Zone: "", + }, + }) + assert.NoError(t, err) +} + +func TestReconcilePodENI_detachMemberENI(t *testing.T) { + m := mocks.NewInterface(t) + m.On("WaitForNetworkInterface", mock.Anything, "foo", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + m.On("DetachNetworkInterface", mock.Anything, "foo", "i-x", "eni-x").Return(nil) + + c := &ReconcilePodENI{ + client: fake.NewClientBuilder().Build(), + aliyun: m, + record: record.NewFakeRecorder(10), + } + err := c.detachMemberENI(context.Background(), &v1beta1.PodENI{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: v1beta1.PodENISpec{ + Allocations: []v1beta1.Allocation{ + { + ENI: v1beta1.ENI{ID: "foo"}, + }, + }, + Zone: "", + }, + Status: v1beta1.PodENIStatus{ + Phase: v1beta1.ENIPhaseBind, + InstanceID: "i-x", + TrunkENIID: "eni-x", + Msg: "", + PodLastSeen: metav1.Time{}, + ENIInfos: nil, + }, + }) + assert.NoError(t, err) +} diff --git a/pkg/controller/pod/pod_controller.go b/pkg/controller/pod/pod_controller.go index b83c381e..70988cd8 100644 --- a/pkg/controller/pod/pod_controller.go +++ b/pkg/controller/pod/pod_controller.go @@ -331,7 +331,7 @@ func (m *ReconcilePod) deleteAllENI(ctx context.Context, podENI *v1beta1.PodENI) if alloc.ENI.ID == "" { continue } - err := m.aliyun.DeleteNetworkInterface(common.WithCtx(ctx, &alloc), alloc.ENI.ID) + err := m.aliyun.DeleteNetworkInterface(ctx, alloc.ENI.ID) if err != nil { return err } @@ -546,8 +546,6 @@ func (m *ReconcilePod) createENI(ctx context.Context, allocs *[]*v1beta1.Allocat ii := i g.Go(func() error { alloc := (*allocs)[ii] - ctx := common.WithCtx(ctx, alloc) - deleteENIOnECSRelease := true if allocType.Type == v1beta1.IPAllocTypeFixed { deleteENIOnECSRelease = false diff --git a/pkg/controller/pod/pod_controller_test.go b/pkg/controller/pod/pod_controller_test.go index 1c2123de..0bf87507 100644 --- a/pkg/controller/pod/pod_controller_test.go +++ b/pkg/controller/pod/pod_controller_test.go @@ -1,5 +1,3 @@ -//go:build test_env - package pod import ( diff --git a/pkg/controller/pod/suite_test.go b/pkg/controller/pod/suite_test.go index c60ebc65..15a7cd38 100644 --- a/pkg/controller/pod/suite_test.go +++ b/pkg/controller/pod/suite_test.go @@ -1,5 +1,3 @@ -//go:build test_env - package pod import ( diff --git a/pkg/eni/crdv2_test.go b/pkg/eni/crdv2_test.go index f9c599df..36ee7a09 100644 --- a/pkg/eni/crdv2_test.go +++ b/pkg/eni/crdv2_test.go @@ -10,6 +10,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" + pkgclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" networkv1beta1 "github.com/AliyunContainerService/terway/pkg/apis/network.alibabacloud.com/v1beta1" @@ -470,3 +471,64 @@ func Test_removeDeleted(t *testing.T) { }) } } + +func TestSyncNodeRuntimeReturnsNilWhenNoDeletedPods(t *testing.T) { + r := &CRDV2{ + deletedPods: make(map[string]*networkv1beta1.RuntimePodStatus), + } + err := r.syncNodeRuntime(context.Background()) + assert.NoError(t, err) +} + +func TestSyncNodeRuntimeReturnsErrorWhenGetRuntimeNodeFails(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(types.Scheme).Build() + r := &CRDV2{ + client: client, + deletedPods: map[string]*networkv1beta1.RuntimePodStatus{"pod-1": {PodID: "pod-1"}}, + } + err := r.syncNodeRuntime(context.Background()) + assert.Error(t, err) +} + +func TestSyncNodeRuntimeUpdatesDeletedPods(t *testing.T) { + n := &networkv1beta1.NodeRuntime{ + ObjectMeta: metav1.ObjectMeta{Name: "node1"}, + Spec: networkv1beta1.NodeRuntimeSpec{}, + Status: networkv1beta1.NodeRuntimeStatus{Pods: map[string]*networkv1beta1.RuntimePodStatus{}}, + } + client := fake.NewClientBuilder().WithScheme(types.Scheme). + WithStatusSubresource(n). + WithObjects(n).Build() + r := &CRDV2{ + client: client, + nodeName: "node1", + deletedPods: map[string]*networkv1beta1.RuntimePodStatus{"pod-1": {PodID: "pod-1"}}, + } + + err := r.syncNodeRuntime(context.Background()) + assert.NoError(t, err) + + nodeRuntime := &networkv1beta1.NodeRuntime{} + err = client.Get(context.Background(), pkgclient.ObjectKey{Name: "node1"}, nodeRuntime) + assert.NoError(t, err) + assert.Contains(t, nodeRuntime.Status.Pods, "pod-1") + assert.Contains(t, nodeRuntime.Status.Pods["pod-1"].Status, networkv1beta1.CNIStatusDeleted) +} + +func TestSyncNodeRuntimeClearsDeletedPods(t *testing.T) { + n := &networkv1beta1.NodeRuntime{ + ObjectMeta: metav1.ObjectMeta{Name: "node1"}, + Spec: networkv1beta1.NodeRuntimeSpec{}, + Status: networkv1beta1.NodeRuntimeStatus{Pods: map[string]*networkv1beta1.RuntimePodStatus{}}, + } + + client := fake.NewClientBuilder().WithScheme(types.Scheme).WithStatusSubresource(n).WithObjects(n).Build() + r := &CRDV2{ + client: client, + nodeName: "node1", + deletedPods: map[string]*networkv1beta1.RuntimePodStatus{"pod-1": {PodID: "pod-1"}}, + } + err := r.syncNodeRuntime(context.Background()) + assert.NoError(t, err) + assert.Empty(t, r.deletedPods) +} diff --git a/pkg/eni/remote_test.go b/pkg/eni/remote_test.go index 934b796f..0e6e3aeb 100644 --- a/pkg/eni/remote_test.go +++ b/pkg/eni/remote_test.go @@ -1,46 +1,49 @@ package eni import ( + "context" "net" "testing" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/AliyunContainerService/terway/types" "github.com/AliyunContainerService/terway/types/daemon" - podENITypes "github.com/AliyunContainerService/terway/pkg/apis/network.alibabacloud.com/v1beta1" + networkv1beta1 "github.com/AliyunContainerService/terway/pkg/apis/network.alibabacloud.com/v1beta1" ) func TestToRPC(t *testing.T) { t.Run("test with valid IPv4 and IPv6 allocations", func(t *testing.T) { l := &RemoteIPResource{ - podENI: podENITypes.PodENI{ - Spec: podENITypes.PodENISpec{ - Allocations: []podENITypes.Allocation{ + podENI: networkv1beta1.PodENI{ + Spec: networkv1beta1.PodENISpec{ + Allocations: []networkv1beta1.Allocation{ { IPv4: "192.168.1.1", IPv4CIDR: "192.168.1.0/24", IPv6: "fd00:db8::1", IPv6CIDR: "fd00:db8::/64", - ENI: podENITypes.ENI{ + ENI: networkv1beta1.ENI{ ID: "eni-11", MAC: "00:00:00:00:00:00", }, Interface: "eth0", - ExtraRoutes: []podENITypes.Route{}, + ExtraRoutes: []networkv1beta1.Route{}, DefaultRoute: true, }, }, }, - Status: podENITypes.PodENIStatus{ + Status: networkv1beta1.PodENIStatus{ Phase: "", InstanceID: "i-123456", TrunkENIID: "eni-12345678", Msg: "", PodLastSeen: metav1.Time{}, - ENIInfos: map[string]podENITypes.ENIInfo{ + ENIInfos: map[string]networkv1beta1.ENIInfo{ "eni-11": {}, }, }, @@ -68,3 +71,51 @@ func TestToRPC(t *testing.T) { assert.Equal(t, true, result[0].DefaultRoute) }) } + +func TestAllocateReturnsErrorWhenResourceTypeMismatch(t *testing.T) { + r := &Remote{} + resp, traces := r.Allocate(context.Background(), &daemon.CNI{}, &LocalIPResource{}) + assert.Nil(t, resp) + assert.Equal(t, ResourceTypeMismatch, traces[0].Condition) +} + +func TestAllocateReturnsNetworkResourcesWhenPodENIReady(t *testing.T) { + scheme := runtime.NewScheme() + _ = networkv1beta1.AddToScheme(scheme) + // Build the fake client with scheme and objects + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(&networkv1beta1.PodENI{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "default", + }, + Spec: networkv1beta1.PodENISpec{ + Allocations: []networkv1beta1.Allocation{ + { + IPv4: "192.168.1.1", + IPv4CIDR: "192.168.1.0/24", + ENI: networkv1beta1.ENI{ + ID: "eni-1", + MAC: "00:00:00:00:00:00", + }, + Interface: "eth0", + DefaultRoute: true, + }, + }, + }, + Status: networkv1beta1.PodENIStatus{ + Phase: networkv1beta1.ENIPhaseBind, + InstanceID: "i-123456", + }, + }). + Build() + + r := NewRemote(client, nil) + cni := &daemon.CNI{PodNamespace: "default", PodName: "pod-1"} + resp, _ := r.Allocate(context.Background(), cni, &RemoteIPRequest{}) + result := <-resp + assert.NoError(t, result.Err) + assert.NotNil(t, result.NetworkConfigs) + assert.Equal(t, "192.168.1.1", result.NetworkConfigs[0].ToRPC()[0].BasicInfo.PodIP.IPv4) +} diff --git a/pkg/ip/ip_test.go b/pkg/ip/ip_test.go index 9eb494e3..4c6e3951 100644 --- a/pkg/ip/ip_test.go +++ b/pkg/ip/ip_test.go @@ -2,7 +2,10 @@ package ip import ( "net" + "net/netip" "testing" + + "github.com/stretchr/testify/assert" ) func Test_ipIntersect(t *testing.T) { @@ -53,3 +56,23 @@ func Test_ipIntersect(t *testing.T) { }) } } + +func TestIPAddrs2str_MultipleValidIPs_ReturnsCorrectStrings(t *testing.T) { + ip1, _ := netip.ParseAddr("192.0.2.1") + ip2, _ := netip.ParseAddr("192.0.2.2") + input := []netip.Addr{ip1, ip2} + expected := []string{"192.0.2.1", "192.0.2.2"} + result := IPAddrs2str(input) + if len(result) != len(expected) { + t.Errorf("Expected %v, got %v", expected, result) + } + for i := range expected { + if result[i] != expected[i] { + t.Errorf("Expected %v, got %v", expected, result) + } + } +} + +func TestDeriveGatewayIP(t *testing.T) { + assert.Equal(t, "192.168.0.253", DeriveGatewayIP("192.168.0.0/24")) +} diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go new file mode 100644 index 00000000..342f4aa2 --- /dev/null +++ b/pkg/k8s/k8s_test.go @@ -0,0 +1,226 @@ +package k8s + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/AliyunContainerService/terway/deviceplugin" + "github.com/AliyunContainerService/terway/types" + "github.com/AliyunContainerService/terway/types/daemon" +) + +func TestGetNodeReturnsNodeWhenExists(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "node1"}, + }).Build() + node, err := getNode(context.Background(), client, "node1") + assert.NoError(t, err) + assert.NotNil(t, node) + assert.Equal(t, "node1", node.Name) +} + +func TestGetPodReturnsPodWhenExists(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"}, + }).Build() + pod, err := getPod(context.Background(), client, "default", "pod1", true) + assert.NoError(t, err) + assert.NotNil(t, pod) + assert.Equal(t, "pod1", pod.Name) +} + +func TestGetCMReturnsConfigMapWhenExists(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm1", Namespace: "default"}, + }).Build() + cm, err := getCM(context.Background(), client, "default", "cm1") + assert.NoError(t, err) + assert.NotNil(t, cm) + assert.Equal(t, "cm1", cm.Name) +} + +func TestConvertPodReturnsPodInfoWithCorrectNetworkType(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"}, + Status: corev1.PodStatus{ + PodIP: "192.168.1.1", + PodIPs: []corev1.PodIP{ + {IP: "192.168.1.1"}, + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, false, sets.New[string](), pod) + assert.Equal(t, daemon.PodNetworkTypeENIMultiIP, result.PodNetworkType) +} + +func TestConvertPodSetsIngressAndEgressBandwidth(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + podIngressBandwidth: "1M", + podEgressBandwidth: "2M", + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, false, sets.New[string](), pod) + assert.Equal(t, uint64(1*MEGABYTE), result.TcIngress) + assert.Equal(t, uint64(2*MEGABYTE), result.TcEgress) +} + +func TestConvertPodHandlesInvalidBandwidthAnnotations(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + podIngressBandwidth: "invalid", + podEgressBandwidth: "invalid", + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, false, sets.New[string](), pod) + assert.Equal(t, uint64(0), result.TcIngress) + assert.Equal(t, uint64(0), result.TcEgress) +} + +func TestConvertPodSetsPodENI(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + types.PodENI: "true", + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, false, sets.New[string](), pod) + assert.True(t, result.PodENI) +} + +func TestConvertPodHandlesInvalidPodENIAnnotation(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + types.PodENI: "invalid", + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, false, sets.New[string](), pod) + assert.False(t, result.PodENI) +} + +func TestConvertPodSetsNetworkPriority(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + types.NetworkPriority: string(types.NetworkPrioGuaranteed), + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, false, sets.New[string](), pod) + assert.Equal(t, string(types.NetworkPrioGuaranteed), result.NetworkPriority) +} + +func TestConvertPodHandlesInvalidNetworkPriorityAnnotation(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + types.NetworkPriority: "invalid", + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, false, sets.New[string](), pod) + assert.Empty(t, result.NetworkPriority) +} + +func TestConvertPodSetsERdmaWhenEnabled(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + deviceplugin.ERDMAResName: resource.MustParse("1"), + }, + }, + }, + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, true, sets.New[string](), pod) + assert.True(t, result.ERdma) +} + +func TestConvertPodSetsIPStickTimeForStatefulWorkload(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + types.PodIPReservation: "true", + }, + }, + } + result := convertPod(daemon.ModeENIMultiIP, false, sets.New[string](), pod) + assert.Equal(t, defaultStickTimeForSts, result.IPStickTime) +} + +func TestServiceCidrFromAPIServerReturnsErrorWhenConfigMapNotFound(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() + _, err := serviceCidrFromAPIServer(client) + assert.Error(t, err) +} + +func TestServiceCidrFromAPIServerReturnsErrorWhenConfigMapDataMissing(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: k8sKubeadmConfigmap, Namespace: k8sSystemNamespace}, + }).Build() + _, err := serviceCidrFromAPIServer(client) + assert.Error(t, err) +} + +func TestServiceCidrFromAPIServerReturnsErrorWhenUnmarshalFails(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: k8sKubeadmConfigmap, Namespace: k8sSystemNamespace}, + Data: map[string]string{k8sKubeadmConfigmapNetworking: "invalid-yaml"}, + }).Build() + _, err := serviceCidrFromAPIServer(client) + assert.Error(t, err) +} + +func TestServiceCidrFromAPIServerReturnsErrorWhenServiceSubnetNotFound(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: k8sKubeadmConfigmap, Namespace: k8sSystemNamespace}, + Data: map[string]string{k8sKubeadmConfigmapNetworking: "networking: {}"}, + }).Build() + _, err := serviceCidrFromAPIServer(client) + assert.Error(t, err) +} + +func TestServiceCidrFromAPIServerReturnsParsedCidr(t *testing.T) { + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: k8sKubeadmConfigmap, Namespace: k8sSystemNamespace}, + Data: map[string]string{k8sKubeadmConfigmapNetworking: "networking:\n serviceSubnet: 10.96.0.0/12"}, + }).Build() + cidr, err := serviceCidrFromAPIServer(client) + assert.NoError(t, err) + assert.NotNil(t, cidr) + assert.Equal(t, "10.96.0.0/12", cidr.String()) +} diff --git a/types/controlplane/annotations_test.go b/types/controlplane/annotations_test.go new file mode 100644 index 00000000..a17f4fbf --- /dev/null +++ b/types/controlplane/annotations_test.go @@ -0,0 +1,83 @@ +package controlplane + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/AliyunContainerService/terway/pkg/apis/network.alibabacloud.com/v1beta1" +) + +func TestParsePodNetworksFromAnnotation(t *testing.T) { + tests := []struct { + name string + podAnnotations map[string]string + expectedResult *PodNetworksAnnotation + expectedError string + }{ + { + name: "AnnotationDoesNotExist", + podAnnotations: map[string]string{}, + expectedResult: &PodNetworksAnnotation{}, + expectedError: "", + }, + { + name: "AnnotationExistsAndValid", + podAnnotations: map[string]string{ + "k8s.aliyun.com/pod-networks": `{ "podNetworks": [{"vSwitchOptions": [ + "vsw-a","vsw-b","vsw-c" + ], "interface": "eth0", + "securityGroupIDs": [ + "sg-1" + ]}]}`, + }, + expectedResult: &PodNetworksAnnotation{ + PodNetworks: []PodNetworks{ + { + Interface: "eth0", + VSwitchOptions: []string{ + "vsw-a", "vsw-b", "vsw-c", + }, + SecurityGroupIDs: []string{ + "sg-1", + }, + }, + }, + }, + expectedError: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: test.podAnnotations, + }, + } + + result, err := ParsePodNetworksFromAnnotation(pod) + if test.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), test.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expectedResult, result) + } + }) + } +} + +func TestReturnsParsedAllocTypeWhenStringIsValidJSON(t *testing.T) { + input := `{"Type": "Fixed", "ReleaseStrategy": "Never", "ReleaseAfter": "5m"}` + expected := &v1beta1.AllocationType{ + Type: v1beta1.IPAllocTypeFixed, + ReleaseStrategy: v1beta1.ReleaseStrategyNever, + ReleaseAfter: "5m", + } + result, err := ParsePodIPType(input) + assert.NoError(t, err) + assert.Equal(t, expected, result) +} diff --git a/types/controlplane/config.go b/types/controlplane/config.go index a3787697..20ee34db 100644 --- a/types/controlplane/config.go +++ b/types/controlplane/config.go @@ -93,10 +93,6 @@ func ParseAndValidate(configFilePath, credentialFilePath string) (*Config, error } } - if c.RegisterEndpoint { - c.Controllers = append(c.Controllers, "endpoint") - } - err = validator.New().Struct(&c) if err != nil { return nil, err diff --git a/types/controlplane/config_default.go b/types/controlplane/config_default.go index 4533e4b1..30f35284 100644 --- a/types/controlplane/config_default.go +++ b/types/controlplane/config_default.go @@ -43,7 +43,6 @@ type Config struct { CertDir string `json:"certDir" validate:"required" mod:"default=/var/run/webhook-cert"` WebhookURLMode bool `json:"webhookURLMode"` LeaderElection bool `json:"leaderElection"` - RegisterEndpoint bool `json:"registerEndpoint"` // deprecated EnableTrace bool `json:"enableTrace"` PodMaxConcurrent int `json:"podMaxConcurrent" validate:"gt=0,lte=10000" mod:"default=10"` diff --git a/types/controlplane/config_test.go b/types/controlplane/config_test.go index 7b78c551..6864465d 100644 --- a/types/controlplane/config_test.go +++ b/types/controlplane/config_test.go @@ -17,9 +17,11 @@ limitations under the License. package controlplane import ( + "os" "testing" "github.com/go-playground/validator/v10" + "github.com/stretchr/testify/assert" ) func TestParseAndValidateCredential(t *testing.T) { @@ -146,3 +148,39 @@ func TestIsControllerEnabled(t *testing.T) { }) } } + +func TestParseAndValidate(t *testing.T) { + configFile, err := os.CreateTemp("", "") + assert.NoError(t, err) + defer os.Remove(configFile.Name()) + + err = os.WriteFile(configFile.Name(), []byte(`disableWebhook: true +regionID: "cn-hangzhou" +leaseLockName: "terway-controller-lock" +leaseLockNamespace: "kube-system" +controllerNamespace: "kube-system" +controllerName: "terway-controlplane" +metricsBindAddress: "127.0.0.1:9999" +healthzBindAddress: "0.0.0.0:8080" +clusterDomain: "cluster.local" +clusterID: foo +vpcID: bar +disableWebhook: true +webhookURLMode: true +leaderElection: true +webhookPort: 4443`), os.ModeType) + assert.NoError(t, err) + + credentialFilePath, err := os.CreateTemp("", "") + assert.NoError(t, err) + defer os.Remove(credentialFilePath.Name()) + + err = os.WriteFile(credentialFilePath.Name(), []byte(`accessKey: foo +accessSecret: bar`), os.ModeType) + assert.NoError(t, err) + + cfg, err := ParseAndValidate(configFile.Name(), credentialFilePath.Name()) + assert.NoError(t, err) + + assert.Equal(t, "cn-hangzhou", cfg.RegionID) +} diff --git a/types/daemon/config_test.go b/types/daemon/config_test.go index d2b8a6a3..58818472 100644 --- a/types/daemon/config_test.go +++ b/types/daemon/config_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/AliyunContainerService/terway/types" ) func Test_MergeConfigAndUnmarshal(t *testing.T) { @@ -105,3 +107,63 @@ func TestGetAddonSecret(t *testing.T) { assert.Equal(t, "key", ak) assert.Equal(t, "secret", sk) } + +func TestGetVSwitchIDsReturnsAllVSwitchIDs(t *testing.T) { + cfg := &Config{ + VSwitches: map[string][]string{ + "zone-a": {"vsw-1", "vsw-2"}, + "zone-b": {"vsw-3"}, + }, + } + vsws := cfg.GetVSwitchIDs() + assert.ElementsMatch(t, []string{"vsw-1", "vsw-2", "vsw-3"}, vsws) +} + +func TestGetVSwitchIDsReturnsEmptyWhenNoVSwitches(t *testing.T) { + cfg := &Config{ + VSwitches: map[string][]string{}, + } + vsws := cfg.GetVSwitchIDs() + assert.Empty(t, vsws) +} + +func TestGetExtraRoutesReturnsAllRoutes(t *testing.T) { + cfg := &Config{ + VSwitches: map[string][]string{ + "zone-a": {"vsw-1", "vsw-2"}, + "zone-b": {"vsw-3"}, + }, + } + routes := cfg.GetExtraRoutes() + assert.ElementsMatch(t, []string{"vsw-1", "vsw-2", "vsw-3"}, routes) +} + +func TestGetExtraRoutesReturnsEmptyWhenNoRoutes(t *testing.T) { + cfg := &Config{ + VSwitches: map[string][]string{}, + } + routes := cfg.GetExtraRoutes() + assert.Empty(t, routes) +} + +func TestPopulateSetsDefaultValues(t *testing.T) { + cfg := &Config{} + cfg.Populate() + assert.Equal(t, 1.0, cfg.EniCapRatio) + assert.Equal(t, VSwitchSelectionPolicyRandom, cfg.VSwitchSelectionPolicy) + assert.Equal(t, string(types.IPStackIPv4), cfg.IPStack) +} + +func TestPopulateDoesNotOverrideExistingValues(t *testing.T) { + cfg := &Config{ + EniCapRatio: 0.5, + VSwitchSelectionPolicy: "custom", + IPStack: string(types.IPStackDual), + } + cfg.Populate() + err := cfg.Validate() + assert.NoError(t, err) + assert.Equal(t, 0.5, cfg.EniCapRatio) + assert.Equal(t, "custom", cfg.VSwitchSelectionPolicy) + assert.Equal(t, string(types.IPStackDual), cfg.IPStack) +} diff --git a/types/daemon/dynamicconfig_test.go b/types/daemon/dynamicconfig_test.go new file mode 100644 index 00000000..43259de8 --- /dev/null +++ b/types/daemon/dynamicconfig_test.go @@ -0,0 +1,56 @@ +package daemon + +import ( + "context" + "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/client/fake" +) + +func TestConfigFromConfigMapReturnsErrorWhenBaseConfigMapNotFound(t *testing.T) { + client := fake.NewFakeClient() + _, err := ConfigFromConfigMap(context.Background(), client, "") + assert.Error(t, err) +} + +func TestConfigFromConfigMapReturnsErrorWhenBaseConfigIsEmpty(t *testing.T) { + client := fake.NewFakeClient(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eni-config", + Namespace: "kube-system", + }, + Data: map[string]string{"eni_conf": ""}, + }) + _, err := ConfigFromConfigMap(context.Background(), client, "") + assert.Error(t, err) +} + +func TestConfigFromConfigMapReturnsConfigWhenNodeNameIsNotEmpty(t *testing.T) { + client := fake.NewFakeClient( + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eni-config", + Namespace: "kube-system", + }, + Data: map[string]string{"eni_conf": `{"version": "1"}`}, + }, + ) + cfg, err := ConfigFromConfigMap(context.Background(), client, "node-1") + assert.NoError(t, err) + assert.Equal(t, "1", cfg.Version) +} + +func TestConfigFromConfigMapReturnsErrorWhenSecurityGroupsExceedLimit(t *testing.T) { + client := fake.NewFakeClient(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eni-config", + Namespace: "kube-system", + }, + Data: map[string]string{"eni_conf": `{"security_groups": ["sg-1", "sg-2", "sg-3", "sg-4", "sg-5", "sg-6"]}`}, + }) + _, err := ConfigFromConfigMap(context.Background(), client, "") + assert.Error(t, err) +} diff --git a/types/daemon/res_test.go b/types/daemon/res_test.go new file mode 100644 index 00000000..5ce52836 --- /dev/null +++ b/types/daemon/res_test.go @@ -0,0 +1,50 @@ +package daemon + +import ( + "reflect" + "testing" +) + +// TestGetResourceItemByType tests the GetResourceItemByType method of PodResources. +func TestGetResourceItemByType(t *testing.T) { + tests := []struct { + name string + resType string + res []ResourceItem + expected []ResourceItem + }{ + { + name: "MatchingType", + resType: "network", + res: []ResourceItem{ + {Type: "network", ID: "1", ENIID: "eni-1", ENIMAC: "02:12:34:56:78:90", IPv4: "10.0.0.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0700:7344"}, + {Type: "storage", ID: "2", ENIID: "eni-2", ENIMAC: "02:12:34:56:78:91", IPv4: "10.0.0.2", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0700:7345"}, + }, + expected: []ResourceItem{ + {Type: "network", ID: "1", ENIID: "eni-1", ENIMAC: "02:12:34:56:78:90", IPv4: "10.0.0.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0700:7344"}, + }, + }, + { + name: "MultipleMatchingTypes", + resType: "network", + res: []ResourceItem{ + {Type: "network", ID: "1", ENIID: "eni-1", ENIMAC: "02:12:34:56:78:90", IPv4: "10.0.0.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0700:7344"}, + {Type: "network", ID: "2", ENIID: "eni-2", ENIMAC: "02:12:34:56:78:91", IPv4: "10.0.0.2", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0700:7345"}, + }, + expected: []ResourceItem{ + {Type: "network", ID: "1", ENIID: "eni-1", ENIMAC: "02:12:34:56:78:90", IPv4: "10.0.0.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0700:7344"}, + {Type: "network", ID: "2", ENIID: "eni-2", ENIMAC: "02:12:34:56:78:91", IPv4: "10.0.0.2", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0700:7345"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + podResources := PodResources{Resources: test.res} + result := podResources.GetResourceItemByType(test.resType) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("GetResourceItemByType(%s) = %v, want %v", test.resType, result, test.expected) + } + }) + } +} diff --git a/types/helper_test.go b/types/helper_test.go new file mode 100644 index 00000000..d3210ca9 --- /dev/null +++ b/types/helper_test.go @@ -0,0 +1,134 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/AliyunContainerService/terway/rpc" +) + +func TestBuildIPNet_EmptyInputs_ReturnsEmptyIPNetSet(t *testing.T) { + ipNetSet, err := BuildIPNet(nil, nil) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.Nil(t, ipNetSet.IPv4) + assert.Nil(t, ipNetSet.IPv6) +} + +func TestBuildIPNet_PartiallyEmptyInputs_ReturnsEmptyIPNetSet(t *testing.T) { + ip := &rpc.IPSet{IPv4: "192.168.1.1", IPv6: "2001:db8::1"} + ipNetSet, err := BuildIPNet(ip, nil) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.Nil(t, ipNetSet.IPv4) + assert.Nil(t, ipNetSet.IPv6) + + subnet := &rpc.IPSet{IPv4: "192.168.1.0/24", IPv6: "2001:db8::/64"} + ipNetSet, err = BuildIPNet(nil, subnet) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.Nil(t, ipNetSet.IPv4) + assert.Nil(t, ipNetSet.IPv6) +} + +func TestBuildIPNet_ValidInputs_ReturnsCorrectIPNetSet(t *testing.T) { + ip := &rpc.IPSet{IPv4: "192.168.1.1", IPv6: "2001:db8::1"} + subnet := &rpc.IPSet{IPv4: "192.168.1.0/24", IPv6: "2001:db8::/64"} + ipNetSet, err := BuildIPNet(ip, subnet) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.NotNil(t, ipNetSet.IPv4) + assert.NotNil(t, ipNetSet.IPv6) + assert.Equal(t, "192.168.1.1/24", ipNetSet.IPv4.String()) + assert.Equal(t, "2001:db8::1/64", ipNetSet.IPv6.String()) +} + +func TestBuildIPNet_InvalidIP_ReturnsError(t *testing.T) { + ip := &rpc.IPSet{IPv4: "invalid", IPv6: "2001:db8::1"} + subnet := &rpc.IPSet{IPv4: "192.168.1.0/24", IPv6: "2001:db8::/64"} + ipNetSet, err := BuildIPNet(ip, subnet) + assert.Error(t, err) + assert.Nil(t, ipNetSet) +} + +func TestBuildIPNet_InvalidSubnet_ReturnsError(t *testing.T) { + ip := &rpc.IPSet{IPv4: "192.168.1.1", IPv6: "2001:db8::1"} + subnet := &rpc.IPSet{IPv4: "invalid", IPv6: "2001:db8::/64"} + ipNetSet, err := BuildIPNet(ip, subnet) + assert.Error(t, err) + assert.Nil(t, ipNetSet) +} + +func TestBuildIPNet_OnlyIPv4_ReturnsCorrectIPNetSet(t *testing.T) { + ip := &rpc.IPSet{IPv4: "192.168.1.1"} + subnet := &rpc.IPSet{IPv4: "192.168.1.0/24"} + ipNetSet, err := BuildIPNet(ip, subnet) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.NotNil(t, ipNetSet.IPv4) + assert.Nil(t, ipNetSet.IPv6) + assert.Equal(t, "192.168.1.1/24", ipNetSet.IPv4.String()) +} + +func TestBuildIPNet_OnlyIPv6_ReturnsCorrectIPNetSet(t *testing.T) { + ip := &rpc.IPSet{IPv6: "2001:db8::1"} + subnet := &rpc.IPSet{IPv6: "2001:db8::/64"} + ipNetSet, err := BuildIPNet(ip, subnet) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.Nil(t, ipNetSet.IPv4) + assert.NotNil(t, ipNetSet.IPv6) + assert.Equal(t, "2001:db8::1/64", ipNetSet.IPv6.String()) +} + +func TestToIPNetSetReturnsErrorWhenIPIsNil(t *testing.T) { + ipNetSet, err := ToIPNetSet(nil) + assert.Error(t, err) + assert.Nil(t, ipNetSet) +} + +func TestToIPNetSetReturnsCorrectIPNetSetWhenValidIPv4(t *testing.T) { + ip := &rpc.IPSet{IPv4: "192.168.1.0/24"} + ipNetSet, err := ToIPNetSet(ip) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.NotNil(t, ipNetSet.IPv4) + assert.Nil(t, ipNetSet.IPv6) + assert.Equal(t, "192.168.1.0/24", ipNetSet.IPv4.String()) +} + +func TestToIPNetSetReturnsCorrectIPNetSetWhenValidIPv6(t *testing.T) { + ip := &rpc.IPSet{IPv6: "2001:db8::/64"} + ipNetSet, err := ToIPNetSet(ip) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.Nil(t, ipNetSet.IPv4) + assert.NotNil(t, ipNetSet.IPv6) + assert.Equal(t, "2001:db8::/64", ipNetSet.IPv6.String()) +} + +func TestToIPNetSetReturnsCorrectIPNetSetWhenValidIPv4AndIPv6(t *testing.T) { + ip := &rpc.IPSet{IPv4: "192.168.1.0/24", IPv6: "2001:db8::/64"} + ipNetSet, err := ToIPNetSet(ip) + assert.NoError(t, err) + assert.NotNil(t, ipNetSet) + assert.NotNil(t, ipNetSet.IPv4) + assert.NotNil(t, ipNetSet.IPv6) + assert.Equal(t, "192.168.1.0/24", ipNetSet.IPv4.String()) + assert.Equal(t, "2001:db8::/64", ipNetSet.IPv6.String()) +} + +func TestToIPNetSetReturnsErrorWhenInvalidIPv4(t *testing.T) { + ip := &rpc.IPSet{IPv4: "invalid"} + ipNetSet, err := ToIPNetSet(ip) + assert.Error(t, err) + assert.Nil(t, ipNetSet) +} + +func TestToIPNetSetReturnsErrorWhenInvalidIPv6(t *testing.T) { + ip := &rpc.IPSet{IPv6: "invalid"} + ipNetSet, err := ToIPNetSet(ip) + assert.Error(t, err) + assert.Nil(t, ipNetSet) +} diff --git a/types/secret/secret_test.go b/types/secret/secret_test.go new file mode 100644 index 00000000..7a2813af --- /dev/null +++ b/types/secret/secret_test.go @@ -0,0 +1,32 @@ +package secret + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStringReturnsMaskedValue(t *testing.T) { + s := Secret("mysecret") + assert.Equal(t, "******", s.String()) + assert.Equal(t, "mysecret", string((s))) +} + +func TestGoStringReturnsMaskedValue(t *testing.T) { + s := Secret("mysecret") + assert.Equal(t, "******", s.GoString()) +} + +func TestMarshalJSONReturnsMaskedValue(t *testing.T) { + s := Secret("mysecret") + json, err := s.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, `"******"`, string(json)) +} + +func TestMarshalJSONHandlesEmptySecret(t *testing.T) { + s := Secret("") + json, err := s.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, `"******"`, string(json)) +} diff --git a/types/types_test.go b/types/types_test.go index b97c457b..2a5e44ee 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -2,10 +2,12 @@ package types_test import ( "fmt" + "net/netip" "testing" "github.com/stretchr/testify/assert" + "github.com/AliyunContainerService/terway/rpc" "github.com/AliyunContainerService/terway/types" ) @@ -59,3 +61,146 @@ func TestErrorUnwrapReturnsNilWhenNoUnderlyingError(t *testing.T) { assert.Nil(t, err.Unwrap()) } + +func TestIPSet2_String(t *testing.T) { + tests := []struct { + name string + ipset2 types.IPSet2 + expected string + }{ + { + name: "IPv4 and IPv6 valid", + ipset2: types.IPSet2{IPv4: netip.MustParseAddr("192.0.2.1"), IPv6: netip.MustParseAddr("2001:db8::1")}, + expected: "192.0.2.1-2001:db8::1", + }, + { + name: "Only IPv4 valid", + ipset2: types.IPSet2{IPv4: netip.MustParseAddr("192.0.2.1")}, + expected: "192.0.2.1", + }, + { + name: "Only IPv6 valid", + ipset2: types.IPSet2{IPv6: netip.MustParseAddr("2001:db8::1")}, + expected: "2001:db8::1", + }, + { + name: "Both invalid", + ipset2: types.IPSet2{}, + expected: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.ipset2.String() + assert.Equal(t, test.expected, result) + }) + } +} + +func TestIPSet2_ToRPC(t *testing.T) { + tests := []struct { + name string + ipset2 types.IPSet2 + expected *rpc.IPSet + }{ + { + name: "IPv4 and IPv6 valid", + ipset2: types.IPSet2{ + IPv4: netip.MustParseAddr("192.0.2.1"), + IPv6: netip.MustParseAddr("2001:db8::1"), + }, + expected: &rpc.IPSet{ + IPv4: "192.0.2.1", + IPv6: "2001:db8::1", + }, + }, + { + name: "Only IPv4 valid", + ipset2: types.IPSet2{ + IPv4: netip.MustParseAddr("192.0.2.1"), + }, + expected: &rpc.IPSet{ + IPv4: "192.0.2.1", + IPv6: "", + }, + }, + { + name: "Only IPv6 valid", + ipset2: types.IPSet2{ + IPv6: netip.MustParseAddr("2001:db8::1"), + }, + expected: &rpc.IPSet{ + IPv4: "", + IPv6: "2001:db8::1", + }, + }, + { + name: "Both invalid", + ipset2: types.IPSet2{}, + expected: &rpc.IPSet{ + IPv4: "", + IPv6: "", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.ipset2.ToRPC() + assert.Equal(t, test.expected, result) + }) + } +} + +func TestIPSet2_GetIPv4(t *testing.T) { + tests := []struct { + name string + ipset2 types.IPSet2 + expected string + }{ + { + name: "IPv4 valid", + ipset2: types.IPSet2{IPv4: netip.MustParseAddr("192.0.2.1")}, + expected: "192.0.2.1", + }, + { + name: "IPv4 invalid", + ipset2: types.IPSet2{}, + expected: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.ipset2.GetIPv4() + assert.Equal(t, test.expected, result) + }) + } +} + +func TestIPSet2_GetIPv6(t *testing.T) { + tests := []struct { + name string + ipset2 types.IPSet2 + expected string + }{ + { + name: "IPv6 valid", + ipset2: types.IPSet2{IPv6: netip.MustParseAddr("2001:db8::1")}, + expected: "2001:db8::1", + }, + { + name: "IPv6 invalid", + ipset2: types.IPSet2{}, + expected: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.ipset2.GetIPv6() + assert.Equal(t, test.expected, result) + }) + } +}