diff --git a/pkg/k8s/node_utils.go b/pkg/k8s/node_utils.go index 9888c5681b..7fd8c142de 100644 --- a/pkg/k8s/node_utils.go +++ b/pkg/k8s/node_utils.go @@ -8,6 +8,7 @@ import ( ) var awsInstanceIDRegex = regexp.MustCompile("^i-[^/]*$") +var eksHybridIDRegex = regexp.MustCompile("^mi-[^/]*$") // GetNodeCondition will get pointer to Node's existing condition. // returns nil if no matching condition found. @@ -26,6 +27,18 @@ func ExtractNodeInstanceID(node *corev1.Node) (string, error) { return "", errors.Errorf("providerID is not specified for node: %s", node.Name) } + // Check if this is a hybrid node + if strings.HasPrefix(providerID, "eks-hybrid://") { + providerIDParts := strings.Split(providerID, "/") + hybridID := providerIDParts[len(providerIDParts)-1] + if !eksHybridIDRegex.MatchString(hybridID) { + return "", errors.Errorf("providerID %s is invalid for EKS hybrid instances, node: %s", providerID, node.Name) + } + // Return a special prefix to identify hybrid nodes + return "hybrid-" + hybridID, nil + } + + // Handle EC2 instances (existing logic) providerIDParts := strings.Split(providerID, "/") instanceID := providerIDParts[len(providerIDParts)-1] if !awsInstanceIDRegex.MatchString(instanceID) { diff --git a/pkg/k8s/node_utils_test.go b/pkg/k8s/node_utils_test.go index 34c92fe44f..32550d779a 100644 --- a/pkg/k8s/node_utils_test.go +++ b/pkg/k8s/node_utils_test.go @@ -102,6 +102,20 @@ func TestExtractNodeInstanceID(t *testing.T) { }, want: "i-abcdefg0", }, + { + name: "node by EKS Hybrid", + args: args{ + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hybrid-node-name", + }, + Spec: corev1.NodeSpec{ + ProviderID: "eks-hybrid:///ap-northeast-1/eks-test/mi-01c49bb1234567890", + }, + }, + }, + want: "hybrid-mi-01c49bb1234567890", + }, { name: "node by EKS Fargate", args: args{ diff --git a/pkg/networking/networking_manager.go b/pkg/networking/networking_manager.go index e43dbdb494..6ed2cc636a 100644 --- a/pkg/networking/networking_manager.go +++ b/pkg/networking/networking_manager.go @@ -161,6 +161,10 @@ func (m *defaultNetworkingManager) computeIngressPermissionsPerSGWithPodEndpoint podsBySG := make(map[string][]k8s.PodInfo) for podKey, eniInfo := range eniInfoByPodKey { + // Handle hybrid pods specially - they don't have security groups + if eniInfo.NetworkInterfaceID == "hybrid-no-eni" { + continue + } sgID, err := m.resolveEndpointSGForENI(ctx, eniInfo) if err != nil { return nil, err diff --git a/pkg/networking/node_info_provider.go b/pkg/networking/node_info_provider.go index 80b5bba64d..60ee184bc7 100644 --- a/pkg/networking/node_info_provider.go +++ b/pkg/networking/node_info_provider.go @@ -46,6 +46,9 @@ func (p *defaultNodeInfoProvider) FetchNodeInstances(ctx context.Context, nodes } nodeKeysByInstanceID := make(map[string][]types.NamespacedName, len(nodes)) for _, node := range nodes { + if node.Labels["eks.amazonaws.com/compute-type"] == "hybrid" { + continue + } instanceID, err := k8s.ExtractNodeInstanceID(node) if err != nil { return nil, err @@ -53,6 +56,12 @@ func (p *defaultNodeInfoProvider) FetchNodeInstances(ctx context.Context, nodes nodeKey := k8s.NamespacedName(node) nodeKeysByInstanceID[instanceID] = append(nodeKeysByInstanceID[instanceID], nodeKey) } + + // If no EC2 instances to fetch, return empty result + if len(nodeKeysByInstanceID) == 0 { + return make(map[types.NamespacedName]*ec2types.Instance), nil + } + instanceIDs := sets.StringKeySet(nodeKeysByInstanceID).List() req := &ec2sdk.DescribeInstancesInput{ InstanceIds: instanceIDs, diff --git a/pkg/networking/pod_eni_info_resolver.go b/pkg/networking/pod_eni_info_resolver.go index b70999f73c..d72a87baf3 100644 --- a/pkg/networking/pod_eni_info_resolver.go +++ b/pkg/networking/pod_eni_info_resolver.go @@ -176,6 +176,18 @@ func (r *defaultPodENIInfoResolver) resolvePodsViaCascadedLookup(ctx context.Con } } } + // Hybrid pods don't have ENIs - they're connected via Direct Connect + // We'll handle them specially in the networking manager + if len(podsByComputeType.hybridPods) > 0 { + // Return empty ENI info for hybrid pods - they'll be handled specially + for _, pod := range podsByComputeType.hybridPods { + eniInfoByPodKey[pod.Key] = ENIInfo{ + // Use a special identifier to mark this as a hybrid pod + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + } + } + } return eniInfoByPodKey, nil } @@ -401,14 +413,15 @@ func (r *defaultPodENIInfoResolver) isPodSupportedByNodeENI(pod k8s.PodInfo, nod return false } -// PodsByComputeType groups pods based on their compute type (EC2, Fargate, SageMaker HyperPod) +// PodsByComputeType groups pods based on their compute type (EC2, Fargate, SageMaker HyperPod, Hybrid) type PodsByComputeType struct { ec2Pods []k8s.PodInfo fargatePods []k8s.PodInfo sageMakerHyperPodPods []k8s.PodInfo + hybridPods []k8s.PodInfo } -// classifyPodsByComputeType classifies in to ec2, fargate and sagemaker-hyperpod groups +// classifyPodsByComputeType classifies in to ec2, fargate, sagemaker-hyperpod and hybrid groups func (r *defaultPodENIInfoResolver) classifyPodsByComputeType(ctx context.Context, pods []k8s.PodInfo) (PodsByComputeType, error) { var podsByComputeType PodsByComputeType nodeNameByComputeType := make(map[string]string) @@ -418,9 +431,12 @@ func (r *defaultPodENIInfoResolver) classifyPodsByComputeType(ctx context.Contex podsByComputeType.fargatePods = append(podsByComputeType.fargatePods, pod) } else if nodeNameByComputeType[pod.NodeName] == "sagemaker-hyperpod" { podsByComputeType.sageMakerHyperPodPods = append(podsByComputeType.sageMakerHyperPodPods, pod) + } else if nodeNameByComputeType[pod.NodeName] == "hybrid" { + podsByComputeType.hybridPods = append(podsByComputeType.hybridPods, pod) } else { podsByComputeType.ec2Pods = append(podsByComputeType.ec2Pods, pod) } + continue // Skip the rest of the loop iteration since we already processed this pod } nodeKey := types.NamespacedName{Name: pod.NodeName} @@ -434,6 +450,9 @@ func (r *defaultPodENIInfoResolver) classifyPodsByComputeType(ctx context.Contex } else if node.Labels[labelSageMakerComputeType] == "hyperpod" { podsByComputeType.sageMakerHyperPodPods = append(podsByComputeType.sageMakerHyperPodPods, pod) nodeNameByComputeType[pod.NodeName] = "sagemaker-hyperpod" + } else if node.Labels[labelEKSComputeType] == "hybrid" { + podsByComputeType.hybridPods = append(podsByComputeType.hybridPods, pod) + nodeNameByComputeType[pod.NodeName] = "hybrid" } else { podsByComputeType.ec2Pods = append(podsByComputeType.ec2Pods, pod) nodeNameByComputeType[pod.NodeName] = "ec2" diff --git a/pkg/networking/pod_eni_info_resolver_test.go b/pkg/networking/pod_eni_info_resolver_test.go index 682a37184a..89a888025d 100644 --- a/pkg/networking/pod_eni_info_resolver_test.go +++ b/pkg/networking/pod_eni_info_resolver_test.go @@ -2813,3 +2813,656 @@ func Test_defaultPodENIInfoResolver_isPodSupportedByNodeENI(t *testing.T) { }) } } +// Add these test functions to pod_eni_info_resolver_test.go + +func Test_defaultPodENIInfoResolver_resolveViaCascadedLookup_Hybrid(t *testing.T) { + hybridNodeA := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hybrid-node-a", + Labels: map[string]string{ + "eks.amazonaws.com/compute-type": "hybrid", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "eks-hybrid:///ap-northeast-1/eks-test/mi-03a43a81234567890", + }, + } + hybridNodeB := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hybrid-node-b", + Labels: map[string]string{ + "eks.amazonaws.com/compute-type": "hybrid", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "eks-hybrid:///ap-northeast-1/eks-test/mi-04b52b91345678901", + }, + } + + type env struct { + nodes []*corev1.Node + } + type args struct { + pods []k8s.PodInfo + } + tests := []struct { + name string + env env + args args + want map[types.NamespacedName]ENIInfo + wantErr error + }{ + { + name: "all hybrid pod's ENI resolved with placeholder info", + env: env{ + nodes: []*corev1.Node{hybridNodeA, hybridNodeB}, + }, + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-b", + PodIP: "10.0.0.101", + }, + }, + }, + want: map[types.NamespacedName]ENIInfo{ + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + }, + }, + { + name: "hybrid pods on same node get same placeholder ENI info", + env: env{ + nodes: []*corev1.Node{hybridNodeA}, + }, + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.101", + }, + { + Key: types.NamespacedName{Namespace: "kube-system", Name: "hybrid-pod-3"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc03"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.102", + }, + }, + }, + want: map[types.NamespacedName]ENIInfo{ + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + types.NamespacedName{Namespace: "kube-system", Name: "hybrid-pod-3"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ec2Client := services.NewMockEC2(ctrl) + // No EC2 expectations - hybrid pods should not make EC2 calls + + k8sSchema := runtime.NewScheme() + clientgoscheme.AddToScheme(k8sSchema) + k8sClient := fake.NewClientBuilder().WithScheme(k8sSchema).Build() + for _, node := range tt.env.nodes { + assert.NoError(t, k8sClient.Create(context.Background(), node.DeepCopy())) + } + nodeInfoProvider := NewMockNodeInfoProvider(ctrl) + // No node info provider expectations - hybrid pods don't need instance info + + r := &defaultPodENIInfoResolver{ + ec2Client: ec2Client, + k8sClient: k8sClient, + nodeInfoProvider: nodeInfoProvider, + vpcID: "vpc-0d6d9ee10bd062dcc", + logger: logr.New(&log.NullLogSink{}), + describeNetworkInterfacesIPChunkSize: 2, + } + + // Call resolvePodsViaCascadedLookup directly since hybrid pods use the special path + got, err := r.resolvePodsViaCascadedLookup(context.Background(), tt.args.pods) + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_defaultPodENIInfoResolver_classifyPodsByComputeType_Hybrid(t *testing.T) { + hybridNodeA := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hybrid-node-a", + Labels: map[string]string{ + "eks.amazonaws.com/compute-type": "hybrid", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "eks-hybrid:///ap-northeast-1/eks-test/mi-03a43a81234567890", + }, + } + hybridNodeB := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hybrid-node-b", + Labels: map[string]string{ + "eks.amazonaws.com/compute-type": "hybrid", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "eks-hybrid:///ap-northeast-1/eks-test/mi-04b52b91345678901", + }, + } + ec2NodeA := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ec2-node-a", + }, + Spec: corev1.NodeSpec{ + ProviderID: "aws:///us-west-2a/i-0fa2d0064e848c69a", + }, + } + fargateNodeA := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fargate-node-a", + Labels: map[string]string{ + "eks.amazonaws.com/compute-type": "fargate", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "aws:///us-west-2b/xxxxxxxx/fargate-ip-192-168-128-147.us-west-2.compute.internal", + }, + } + sageMakerNodeA := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sagemaker-node-a", + Labels: map[string]string{ + "sagemaker.amazonaws.com/compute-type": "hyperpod", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "aws:///usw2-az2/sagemaker/cluster/hyperpod-xxxxxxxxxxxx-i-04442beca624ba65b", + }, + } + + type env struct { + nodes []*corev1.Node + } + type args struct { + pods []k8s.PodInfo + } + tests := []struct { + name string + env env + args args + want PodsByComputeType + wantErr error + }{ + { + name: "classify hybrid pods correctly", + env: env{ + nodes: []*corev1.Node{hybridNodeA, hybridNodeB}, + }, + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-b", + PodIP: "10.0.0.101", + }, + }, + }, + want: PodsByComputeType{ + ec2Pods: nil, + fargatePods: nil, + sageMakerHyperPodPods: nil, + hybridPods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-b", + PodIP: "10.0.0.101", + }, + }, + }, + }, + { + name: "classify mixed compute types including hybrid", + env: env{ + nodes: []*corev1.Node{hybridNodeA, ec2NodeA, fargateNodeA, sageMakerNodeA}, + }, + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "ec2-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "ec2-node-a", + PodIP: "192.168.100.1", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "fargate-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc03"), + NodeName: "fargate-node-a", + PodIP: "192.168.128.147", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "sagemaker-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc04"), + NodeName: "sagemaker-node-a", + PodIP: "192.168.128.151", + }, + }, + }, + want: PodsByComputeType{ + ec2Pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "ec2-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "ec2-node-a", + PodIP: "192.168.100.1", + }, + }, + fargatePods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "fargate-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc03"), + NodeName: "fargate-node-a", + PodIP: "192.168.128.147", + }, + }, + sageMakerHyperPodPods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "sagemaker-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc04"), + NodeName: "sagemaker-node-a", + PodIP: "192.168.128.151", + }, + }, + hybridPods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + }, + }, + }, + { + name: "hybrid node caching works correctly - multiple pods on same node", + env: env{ + nodes: []*corev1.Node{hybridNodeA}, + }, + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-a", // Same node as above - should use cache + PodIP: "10.0.0.101", + }, + { + Key: types.NamespacedName{Namespace: "kube-system", Name: "hybrid-pod-3"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc03"), + NodeName: "hybrid-node-a", // Same node as above - should use cache + PodIP: "10.0.0.102", + }, + }, + }, + want: PodsByComputeType{ + ec2Pods: nil, + fargatePods: nil, + sageMakerHyperPodPods: nil, + hybridPods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.101", + }, + { + Key: types.NamespacedName{Namespace: "kube-system", Name: "hybrid-pod-3"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc03"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.102", + }, + }, + }, + }, + { + name: "empty hybrid pods list", + env: env{ + nodes: []*corev1.Node{hybridNodeA}, + }, + args: args{ + pods: []k8s.PodInfo{}, + }, + want: PodsByComputeType{ + ec2Pods: nil, + fargatePods: nil, + sageMakerHyperPodPods: nil, + hybridPods: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + k8sSchema := runtime.NewScheme() + clientgoscheme.AddToScheme(k8sSchema) + k8sClient := fake.NewClientBuilder().WithScheme(k8sSchema).Build() + for _, node := range tt.env.nodes { + assert.NoError(t, k8sClient.Create(context.Background(), node.DeepCopy())) + } + + r := &defaultPodENIInfoResolver{ + k8sClient: k8sClient, + logger: logr.New(&log.NullLogSink{}), + } + + got, err := r.classifyPodsByComputeType(context.Background(), tt.args.pods) + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_defaultPodENIInfoResolver_Resolve_Hybrid(t *testing.T) { + hybridNodeA := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hybrid-node-a", + Labels: map[string]string{ + "eks.amazonaws.com/compute-type": "hybrid", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "eks-hybrid:///ap-northeast-1/eks-test/mi-03a43a81234567890", + }, + } + hybridNodeB := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hybrid-node-b", + Labels: map[string]string{ + "eks.amazonaws.com/compute-type": "hybrid", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "eks-hybrid:///ap-northeast-1/eks-test/mi-04b52b91345678901", + }, + } + + type env struct { + nodes []*corev1.Node + } + type args struct { + pods []k8s.PodInfo + } + type resolveCall struct { + args args + want map[types.NamespacedName]ENIInfo + wantErr error + } + tests := []struct { + name string + env env + wantResolveCalls []resolveCall + }{ + { + name: "successfully resolve hybrid pods with placeholder ENI", + env: env{ + nodes: []*corev1.Node{hybridNodeA, hybridNodeB}, + }, + wantResolveCalls: []resolveCall{ + { + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-b", + PodIP: "10.0.0.101", + }, + }, + }, + want: map[types.NamespacedName]ENIInfo{ + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + }, + }, + }, + }, + { + name: "successfully resolve hybrid pods with cache hit on second call", + env: env{ + nodes: []*corev1.Node{hybridNodeA}, + }, + wantResolveCalls: []resolveCall{ + { + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.101", + }, + }, + }, + want: map[types.NamespacedName]ENIInfo{ + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + }, + }, + { + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.101", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-3"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc03"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.102", + }, + }, + }, + want: map[types.NamespacedName]ENIInfo{ + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-3"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + }, + }, + }, + }, + { + name: "successfully resolve hybrid pods with cache fully hit", + env: env{ + nodes: []*corev1.Node{hybridNodeA}, + }, + wantResolveCalls: []resolveCall{ + { + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc01"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.100", + }, + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.101", + }, + }, + }, + want: map[types.NamespacedName]ENIInfo{ + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-1"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + }, + }, + { + args: args{ + pods: []k8s.PodInfo{ + { + Key: types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}, + UID: types.UID("2d8740a6-f4b1-4074-a91c-f0084ec0bc02"), + NodeName: "hybrid-node-a", + PodIP: "10.0.0.101", + }, + }, + }, + want: map[types.NamespacedName]ENIInfo{ + types.NamespacedName{Namespace: "default", Name: "hybrid-pod-2"}: { + NetworkInterfaceID: "hybrid-no-eni", + SecurityGroups: []string{}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ec2Client := services.NewMockEC2(ctrl) + // No EC2 expectations for hybrid pods + + k8sSchema := runtime.NewScheme() + clientgoscheme.AddToScheme(k8sSchema) + k8sClient := fake.NewClientBuilder().WithScheme(k8sSchema).Build() + for _, node := range tt.env.nodes { + assert.NoError(t, k8sClient.Create(context.Background(), node.DeepCopy())) + } + nodeInfoProvider := NewMockNodeInfoProvider(ctrl) + // No nodeInfoProvider expectations for hybrid pods + + r := NewDefaultPodENIInfoResolver(k8sClient, ec2Client, nodeInfoProvider, "vpc-abc", logr.New(&log.NullLogSink{})) + for _, call := range tt.wantResolveCalls { + got, err := r.Resolve(context.Background(), call.args.pods) + if call.wantErr != nil { + assert.EqualError(t, err, call.wantErr.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, call.want, got) + } + } + }) + } +}