1414package custom_networking
1515
1616import (
17+ "context"
1718 "fmt"
1819 "net"
1920 "strconv"
2021 "time"
2122
2223 awsUtils "github.com/aws/amazon-vpc-cni-k8s/test/framework/resources/aws/utils"
2324 "github.com/aws/amazon-vpc-cni-k8s/test/framework/resources/k8s/manifest"
25+ k8sUtils "github.com/aws/amazon-vpc-cni-k8s/test/framework/resources/k8s/utils"
2426 "github.com/aws/amazon-vpc-cni-k8s/test/framework/utils"
2527
2628 . "github.com/onsi/ginkgo/v2"
2729 . "github.com/onsi/gomega"
2830 v1 "k8s.io/api/apps/v1"
2931 coreV1 "k8s.io/api/core/v1"
32+ "k8s.io/apimachinery/pkg/types"
3033)
3134
3235var _ = Describe ("Custom Networking Test" , func () {
@@ -51,7 +54,6 @@ var _ = Describe("Custom Networking Test", func() {
5154 Command ([]string {"nc" }).
5255 Args ([]string {"-k" , "-l" , strconv .Itoa (port )}).
5356 Build ()
54-
5557 deploymentBuilder := manifest .NewBusyBoxDeploymentBuilder (f .Options .TestImageRegistry ).
5658 Container (container ).
5759 Replicas (replicaCount ).
@@ -90,7 +92,29 @@ var _ = Describe("Custom Networking Test", func() {
9092 Parallelism (1 ).
9193 Build ()
9294
95+ // Force client Job onto a DIFFERENT node than THIS target pod so that
96+ // traffic actually traverses the ENI and AWS SG enforcement applies
97+ // (same-node pod-to-pod bypasses ENI-level SG evaluation in the Linux bridge).
98+ // We only exclude the one target pod's node, not all target nodes, so the
99+ // Job can still schedule on 2-node clusters when targets span both nodes.
100+ testJob .Spec .Template .Spec .Affinity = & coreV1.Affinity {
101+ NodeAffinity : & coreV1.NodeAffinity {
102+ RequiredDuringSchedulingIgnoredDuringExecution : & coreV1.NodeSelector {
103+ NodeSelectorTerms : []coreV1.NodeSelectorTerm {{
104+ MatchExpressions : []coreV1.NodeSelectorRequirement {{
105+ Key : "kubernetes.io/hostname" ,
106+ Operator : coreV1 .NodeSelectorOpNotIn ,
107+ Values : []string {pod .Spec .NodeName },
108+ }},
109+ }},
110+ },
111+ },
112+ }
113+
93114 _ , err := f .K8sResourceManagers .JobManager ().CreateAndWaitTillJobCompleted (testJob )
115+ logJobPodDiag (testJob .Name )
116+ logTargetENIDiag (pod )
117+
94118 if shouldConnect {
95119 By ("verifying connection to pod succeeds on port " + strconv .Itoa (port ))
96120 Expect (err ).ToNot (HaveOccurred ())
@@ -116,6 +140,7 @@ var _ = Describe("Custom Networking Test", func() {
116140 shouldConnect = true
117141 })
118142 It ("should connect" , func () {})
143+
119144 })
120145
121146 Context ("when connecting to unreachable port" , func () {
@@ -128,6 +153,49 @@ var _ = Describe("Custom Networking Test", func() {
128153 })
129154 })
130155
156+ Context ("when a custom-networking pod connects to an external endpoint on an egress-allowed port" , func () {
157+ It ("should succeed reaching 1.1.1.1:53" , func () {
158+ testJob := manifest .NewDefaultJobBuilder ().
159+ Container (manifest .NewNetCatAlpineContainer (f .Options .TestImageRegistry ).
160+ Command ([]string {"nc" }).
161+ Args ([]string {"-z" , "-v" , "-w5" , "1.1.1.1" , "53" }).
162+ Build ()).
163+ Name ("external-reachability" ).
164+ Parallelism (1 ).
165+ Build ()
166+ _ , err := f .K8sResourceManagers .JobManager ().CreateAndWaitTillJobCompleted (testJob )
167+ logJobPodDiag (testJob .Name )
168+ Expect (err ).ToNot (HaveOccurred ())
169+ Expect (f .K8sResourceManagers .JobManager ().DeleteAndWaitTillJobIsDeleted (testJob )).ToNot (HaveOccurred ())
170+ })
171+ })
172+
173+ Context ("when a custom-networking pod connects to the Kubernetes API ClusterIP" , func () {
174+ It ("should reach the API server via ClusterIP" , func () {
175+ // Use the well-known kubernetes.default ClusterIP service which exists on every cluster.
176+ // Resolve the ClusterIP dynamically to avoid hardcoding.
177+ ctx , cancel := context .WithTimeout (context .Background (), 2 * time .Minute )
178+ defer cancel ()
179+
180+ kubeSvc , err := f .K8sResourceManagers .ServiceManager ().GetService (ctx , "default" , "kubernetes" )
181+ Expect (err ).ToNot (HaveOccurred ())
182+ clusterIP := kubeSvc .Spec .ClusterIP
183+
184+ testJob := manifest .NewDefaultJobBuilder ().
185+ Container (manifest .NewNetCatAlpineContainer (f .Options .TestImageRegistry ).
186+ Command ([]string {"nc" }).
187+ Args ([]string {"-z" , "-v" , "-w5" , clusterIP , "443" }).
188+ Build ()).
189+ Name ("k8s-api-clusterip" ).
190+ Parallelism (1 ).
191+ Build ()
192+ _ , err = f .K8sResourceManagers .JobManager ().CreateAndWaitTillJobCompleted (testJob )
193+ logJobPodDiag (testJob .Name )
194+ Expect (err ).ToNot (HaveOccurred ())
195+ Expect (f .K8sResourceManagers .JobManager ().DeleteAndWaitTillJobIsDeleted (testJob )).ToNot (HaveOccurred ())
196+ })
197+ })
198+
131199 Context ("when creating deployment on nodes that do not have ENIConfig" , func () {
132200 JustBeforeEach (func () {
133201 By ("deleting ENIConfig for all availability zones" )
@@ -158,7 +226,7 @@ var _ = Describe("Custom Networking Test", func() {
158226
159227 By ("verifying deployment should not succeed" )
160228 deployment , err = f .K8sResourceManagers .DeploymentManager ().
161- CreateAndWaitTillDeploymentIsReady (deployment , utils .DefaultDeploymentReadyTimeout )
229+ CreateAndWaitTillDeploymentIsReady (deployment , utils .ShortDeploymentReadyTimeout )
162230 Expect (err ).To (HaveOccurred ())
163231
164232 By ("deleting the failed deployment" )
@@ -208,3 +276,46 @@ var _ = Describe("Custom Networking Test", func() {
208276 })
209277 })
210278})
279+
280+ // logJobPodDiag prints pod name, node, IP, phase, and container logs for every
281+ // pod belonging to the given Job. Useful for post-mortem when a Job fails.
282+ func logJobPodDiag (jobName string ) {
283+ pods , err := f .K8sResourceManagers .PodManager ().GetPodsWithLabelSelector ("job-name" , jobName )
284+ if err != nil {
285+ return
286+ }
287+ for _ , p := range pods .Items {
288+ logs , _ := f .K8sResourceManagers .PodManager ().PodLogs (p .Namespace , p .Name )
289+ fmt .Fprintf (GinkgoWriter , "[DIAG] pod=%s node=%s ip=%s phase=%s\n %s\n " ,
290+ p .Name , p .Spec .NodeName , p .Status .PodIP , p .Status .Phase , logs )
291+ }
292+ }
293+
294+ // logTargetENIDiag looks up the EC2 ENI that owns the target pod's IP and
295+ // prints its ENI ID, subnet, and security groups.
296+ func logTargetENIDiag (pod coreV1.Pod ) {
297+ ctx , cancel := context .WithTimeout (context .Background (), 2 * time .Minute )
298+ defer cancel ()
299+
300+ node := & coreV1.Node {}
301+ if err := f .K8sClient .Get (ctx , types.NamespacedName {Name : pod .Spec .NodeName }, node ); err != nil {
302+ return
303+ }
304+ instance , err := f .CloudServices .EC2 ().DescribeInstance (ctx , k8sUtils .GetInstanceIDFromNode (* node ))
305+ if err != nil {
306+ return
307+ }
308+ for _ , nic := range instance .NetworkInterfaces {
309+ for _ , pip := range nic .PrivateIpAddresses {
310+ if pip .PrivateIpAddress != nil && * pip .PrivateIpAddress == pod .Status .PodIP {
311+ var sgs []string
312+ for _ , g := range nic .Groups {
313+ sgs = append (sgs , * g .GroupId )
314+ }
315+ fmt .Fprintf (GinkgoWriter , "[DIAG] target ENI=%s subnet=%s SGs=%v\n " ,
316+ * nic .NetworkInterfaceId , * nic .SubnetId , sgs )
317+ return
318+ }
319+ }
320+ }
321+ }
0 commit comments