k8s.io/kubernetes@v1.29.3/test/e2e/framework/pod/dial.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package pod
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net"
    25  	"net/http"
    26  	"regexp"
    27  	"strconv"
    28  	"time"
    29  
    30  	v1 "k8s.io/api/core/v1"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/httpstream"
    33  	"k8s.io/client-go/kubernetes"
    34  	"k8s.io/client-go/kubernetes/scheme"
    35  	"k8s.io/client-go/rest"
    36  	"k8s.io/client-go/tools/portforward"
    37  	"k8s.io/client-go/transport/spdy"
    38  	"k8s.io/klog/v2"
    39  )
    40  
    41  // NewTransport creates a transport which uses the port forward dialer.
    42  // URLs must use <namespace>.<pod>:<port> as host.
    43  func NewTransport(client kubernetes.Interface, restConfig *rest.Config) *http.Transport {
    44  	return &http.Transport{
    45  		DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
    46  			dialer := NewDialer(client, restConfig)
    47  			a, err := ParseAddr(addr)
    48  			if err != nil {
    49  				return nil, err
    50  			}
    51  			return dialer.DialContainerPort(ctx, *a)
    52  		},
    53  	}
    54  }
    55  
    56  // NewDialer creates a dialer that supports connecting to container ports.
    57  func NewDialer(client kubernetes.Interface, restConfig *rest.Config) *Dialer {
    58  	return &Dialer{
    59  		client:     client,
    60  		restConfig: restConfig,
    61  	}
    62  }
    63  
    64  // Dialer holds the relevant parameters that are independent of a particular connection.
    65  type Dialer struct {
    66  	client     kubernetes.Interface
    67  	restConfig *rest.Config
    68  }
    69  
    70  // DialContainerPort connects to a certain container port in a pod.
    71  func (d *Dialer) DialContainerPort(ctx context.Context, addr Addr) (conn net.Conn, finalErr error) {
    72  	restClient := d.client.CoreV1().RESTClient()
    73  	restConfig := d.restConfig
    74  	if restConfig.GroupVersion == nil {
    75  		restConfig.GroupVersion = &schema.GroupVersion{}
    76  	}
    77  	if restConfig.NegotiatedSerializer == nil {
    78  		restConfig.NegotiatedSerializer = scheme.Codecs
    79  	}
    80  
    81  	// The setup code around the actual portforward is from
    82  	// https://github.com/kubernetes/kubernetes/blob/c652ffbe4a29143623a1aaec39f745575f7e43ad/staging/src/k8s.io/kubectl/pkg/cmd/portforward/portforward.go
    83  	req := restClient.Post().
    84  		Resource("pods").
    85  		Namespace(addr.Namespace).
    86  		Name(addr.PodName).
    87  		SubResource("portforward")
    88  	transport, upgrader, err := spdy.RoundTripperFor(restConfig)
    89  	if err != nil {
    90  		return nil, fmt.Errorf("create round tripper: %w", err)
    91  	}
    92  	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL())
    93  
    94  	streamConn, _, err := dialer.Dial(portforward.PortForwardProtocolV1Name)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("dialer failed: %w", err)
    97  	}
    98  	requestID := "1"
    99  	defer func() {
   100  		if finalErr != nil {
   101  			streamConn.Close()
   102  		}
   103  	}()
   104  
   105  	// create error stream
   106  	headers := http.Header{}
   107  	headers.Set(v1.StreamType, v1.StreamTypeError)
   108  	headers.Set(v1.PortHeader, fmt.Sprintf("%d", addr.Port))
   109  	headers.Set(v1.PortForwardRequestIDHeader, requestID)
   110  
   111  	// We're not writing to this stream, just reading an error message from it.
   112  	// This happens asynchronously.
   113  	errorStream, err := streamConn.CreateStream(headers)
   114  	if err != nil {
   115  		return nil, fmt.Errorf("error creating error stream: %w", err)
   116  	}
   117  	errorStream.Close()
   118  	go func() {
   119  		message, err := io.ReadAll(errorStream)
   120  		switch {
   121  		case err != nil:
   122  			klog.ErrorS(err, "error reading from error stream")
   123  		case len(message) > 0:
   124  			klog.ErrorS(errors.New(string(message)), "an error occurred connecting to the remote port")
   125  		}
   126  	}()
   127  
   128  	// create data stream
   129  	headers.Set(v1.StreamType, v1.StreamTypeData)
   130  	dataStream, err := streamConn.CreateStream(headers)
   131  	if err != nil {
   132  		return nil, fmt.Errorf("error creating data stream: %w", err)
   133  	}
   134  
   135  	return &stream{
   136  		Stream:     dataStream,
   137  		streamConn: streamConn,
   138  	}, nil
   139  }
   140  
   141  // Addr contains all relevant parameters for a certain port in a pod.
   142  // The container should be running before connections are attempted,
   143  // otherwise the connection will fail.
   144  type Addr struct {
   145  	Namespace, PodName string
   146  	Port               int
   147  }
   148  
   149  var _ net.Addr = Addr{}
   150  
   151  func (a Addr) Network() string {
   152  	return "port-forwarding"
   153  }
   154  
   155  func (a Addr) String() string {
   156  	return fmt.Sprintf("%s.%s:%d", a.Namespace, a.PodName, a.Port)
   157  }
   158  
   159  // ParseAddr expects a <namespace>.<pod>:<port number> as produced
   160  // by Addr.String.
   161  func ParseAddr(addr string) (*Addr, error) {
   162  	parts := addrRegex.FindStringSubmatch(addr)
   163  	if parts == nil {
   164  		return nil, fmt.Errorf("%q: must match the format <namespace>.<pod>:<port number>", addr)
   165  	}
   166  	port, _ := strconv.Atoi(parts[3])
   167  	return &Addr{
   168  		Namespace: parts[1],
   169  		PodName:   parts[2],
   170  		Port:      port,
   171  	}, nil
   172  }
   173  
   174  var addrRegex = regexp.MustCompile(`^([^\.]+)\.([^:]+):(\d+)$`)
   175  
   176  type stream struct {
   177  	addr Addr
   178  	httpstream.Stream
   179  	streamConn httpstream.Connection
   180  }
   181  
   182  var _ net.Conn = &stream{}
   183  
   184  func (s *stream) Close() error {
   185  	s.Stream.Close()
   186  	s.streamConn.Close()
   187  	return nil
   188  }
   189  
   190  func (s *stream) LocalAddr() net.Addr {
   191  	return LocalAddr{}
   192  }
   193  
   194  func (s *stream) RemoteAddr() net.Addr {
   195  	return s.addr
   196  }
   197  
   198  func (s *stream) SetDeadline(t time.Time) error {
   199  	return nil
   200  }
   201  
   202  func (s *stream) SetReadDeadline(t time.Time) error {
   203  	return nil
   204  }
   205  
   206  func (s *stream) SetWriteDeadline(t time.Time) error {
   207  	return nil
   208  }
   209  
   210  type LocalAddr struct{}
   211  
   212  var _ net.Addr = LocalAddr{}
   213  
   214  func (l LocalAddr) Network() string { return "port-forwarding" }
   215  func (l LocalAddr) String() string  { return "apiserver" }