Skip to content

Commit dbe6b66

Browse files
nic-6443soltysh
authored andcommitted
fix: draining remote stream after port-forward connection broken
Signed-off-by: Nic <[email protected]>
1 parent 847be85 commit dbe6b66

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

staging/src/k8s.io/client-go/tools/portforward/portforward.go

+5
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,11 @@ func (pf *PortForwarder) handleConnection(conn net.Conn, port ForwardedPort) {
406406
case <-remoteDone:
407407
case <-localError:
408408
}
409+
/*
410+
reset dataStream to discard any unsent data, preventing port forwarding from being blocked.
411+
we must reset dataStream before waiting on errorChan, otherwise, the blocking data will affect errorStream and cause <-errorChan to block indefinitely.
412+
*/
413+
_ = dataStream.Reset()
409414

410415
// always expect something on errorChan (it may be nil)
411416
err = <-errorChan

test/e2e/kubectl/portforward.go

+94
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"fmt"
2626
"io"
2727
"net"
28+
"net/http"
2829
"os/exec"
2930
"regexp"
3031
"strconv"
@@ -123,6 +124,36 @@ func pfPod(expectedClientData, chunks, chunkSize, chunkIntervalMillis string, bi
123124
}
124125
}
125126

127+
func testWebServerPod() *v1.Pod {
128+
return &v1.Pod{
129+
ObjectMeta: metav1.ObjectMeta{
130+
Name: podName,
131+
Labels: map[string]string{"name": podName},
132+
},
133+
Spec: v1.PodSpec{
134+
Containers: []v1.Container{
135+
{
136+
Name: "testwebserver",
137+
Image: imageutils.GetE2EImage(imageutils.Agnhost),
138+
Args: []string{"test-webserver"},
139+
Ports: []v1.ContainerPort{{ContainerPort: int32(80)}},
140+
ReadinessProbe: &v1.Probe{
141+
ProbeHandler: v1.ProbeHandler{
142+
HTTPGet: &v1.HTTPGetAction{
143+
Path: "/",
144+
Port: intstr.FromInt32(int32(80)),
145+
},
146+
},
147+
InitialDelaySeconds: 5,
148+
TimeoutSeconds: 3,
149+
FailureThreshold: 10,
150+
},
151+
},
152+
},
153+
},
154+
}
155+
}
156+
126157
// WaitForTerminatedContainer waits till a given container be terminated for a given pod.
127158
func WaitForTerminatedContainer(ctx context.Context, f *framework.Framework, pod *v1.Pod, containerName string) error {
128159
return e2epod.WaitForPodCondition(ctx, f.ClientSet, f.Namespace.Name, pod.Name, "container terminated", framework.PodStartTimeout, func(pod *v1.Pod) (bool, error) {
@@ -493,6 +524,69 @@ var _ = SIGDescribe("Kubectl Port forwarding", func() {
493524
doTestOverWebSockets(ctx, "localhost", f)
494525
})
495526
})
527+
528+
ginkgo.Describe("Shutdown client connection while the remote stream is writing data to the port-forward connection", func() {
529+
ginkgo.It("port-forward should keep working after detect broken connection", func(ctx context.Context) {
530+
ginkgo.By("Creating the target pod")
531+
pod := testWebServerPod()
532+
if _, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(ctx, pod, metav1.CreateOptions{}); err != nil {
533+
framework.Failf("Couldn't create pod: %v", err)
534+
}
535+
if err := e2epod.WaitTimeoutForPodReadyInNamespace(ctx, f.ClientSet, pod.Name, f.Namespace.Name, framework.PodStartTimeout); err != nil {
536+
framework.Failf("Pod did not start running: %v", err)
537+
}
538+
539+
ginkgo.By("Running 'kubectl port-forward'")
540+
cmd := runPortForward(f.Namespace.Name, pod.Name, 80)
541+
defer cmd.Stop()
542+
543+
ginkgo.By("Send a http request to verify port-forward working")
544+
client := http.Client{
545+
Timeout: 5 * time.Second,
546+
}
547+
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/", cmd.port))
548+
if err != nil {
549+
framework.Failf("Couldn't get http response from port-forward: %v", err)
550+
}
551+
if resp.StatusCode != http.StatusOK {
552+
framework.Failf("Expected status code %d, got %d", http.StatusOK, resp.StatusCode)
553+
}
554+
555+
ginkgo.By("Dialing the local port")
556+
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", cmd.port))
557+
if err != nil {
558+
framework.Failf("Couldn't connect to port %d: %v", cmd.port, err)
559+
}
560+
561+
// use raw tcp connection to emulate client close connection without reading response
562+
ginkgo.By("Request agohost binary file (40MB+)")
563+
requestLines := []string{"GET /agnhost HTTP/1.1", "Host: localhost", ""}
564+
for _, line := range requestLines {
565+
if _, err := conn.Write(append([]byte(line), []byte("\r\n")...)); err != nil {
566+
framework.Failf("Couldn't write http request to local connection: %v", err)
567+
}
568+
}
569+
570+
ginkgo.By("Read only one byte from the connection")
571+
if _, err := conn.Read(make([]byte, 1)); err != nil {
572+
framework.Logf("Couldn't reading from the local connection: %v", err)
573+
}
574+
575+
ginkgo.By("Close client connection without reading remain data")
576+
if err := conn.Close(); err != nil {
577+
framework.Failf("Couldn't close local connection: %v", err)
578+
}
579+
580+
ginkgo.By("Send another http request through port-forward again")
581+
resp, err = client.Get(fmt.Sprintf("http://127.0.0.1:%d/", cmd.port))
582+
if err != nil {
583+
framework.Failf("Couldn't get http response from port-forward: %v", err)
584+
}
585+
if resp.StatusCode != http.StatusOK {
586+
framework.Failf("Expected status code %d, got %d", http.StatusOK, resp.StatusCode)
587+
}
588+
})
589+
})
496590
})
497591

498592
func wsRead(conn *websocket.Conn) (byte, []byte, error) {

0 commit comments

Comments
 (0)