sigs.k8s.io/cluster-api@v1.7.1/controlplane/kubeadm/internal/proxy/dial.go (about)

     1  /*
     2  Copyright 2020 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 proxy
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net"
    23  	"net/http"
    24  	"time"
    25  
    26  	"github.com/pkg/errors"
    27  	corev1 "k8s.io/api/core/v1"
    28  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    29  	"k8s.io/client-go/kubernetes"
    30  	"k8s.io/client-go/tools/portforward"
    31  	"k8s.io/client-go/transport/spdy"
    32  )
    33  
    34  const defaultTimeout = 10 * time.Second
    35  
    36  // Dialer creates connections using Kubernetes API Server port-forwarding.
    37  type Dialer struct {
    38  	proxy          Proxy
    39  	clientset      *kubernetes.Clientset
    40  	proxyTransport http.RoundTripper
    41  	upgrader       spdy.Upgrader
    42  	timeout        time.Duration
    43  }
    44  
    45  // NewDialer creates a new dialer for a given API server scope.
    46  func NewDialer(p Proxy, options ...func(*Dialer) error) (*Dialer, error) {
    47  	if p.Port == 0 {
    48  		return nil, errors.New("port required")
    49  	}
    50  
    51  	dialer := &Dialer{
    52  		proxy: p,
    53  	}
    54  
    55  	for _, option := range options {
    56  		err := option(dialer)
    57  		if err != nil {
    58  			return nil, err
    59  		}
    60  	}
    61  
    62  	if dialer.timeout == 0 {
    63  		dialer.timeout = defaultTimeout
    64  	}
    65  	p.KubeConfig.Timeout = dialer.timeout
    66  	clientset, err := kubernetes.NewForConfig(p.KubeConfig)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	proxyTransport, upgrader, err := spdy.RoundTripperFor(p.KubeConfig)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	dialer.proxyTransport = proxyTransport
    75  	dialer.upgrader = upgrader
    76  	dialer.clientset = clientset
    77  	return dialer, nil
    78  }
    79  
    80  // DialContextWithAddr is a GO grpc compliant dialer construct.
    81  func (d *Dialer) DialContextWithAddr(ctx context.Context, addr string) (net.Conn, error) {
    82  	return d.DialContext(ctx, scheme, addr)
    83  }
    84  
    85  // DialContext creates proxied port-forwarded connections.
    86  // ctx is currently unused, but fulfils the type signature used by GRPC.
    87  func (d *Dialer) DialContext(_ context.Context, _ string, addr string) (net.Conn, error) {
    88  	req := d.clientset.CoreV1().RESTClient().
    89  		Post().
    90  		Resource(d.proxy.Kind).
    91  		Namespace(d.proxy.Namespace).
    92  		Name(addr).
    93  		SubResource("portforward")
    94  
    95  	dialer := spdy.NewDialer(d.upgrader, &http.Client{Transport: d.proxyTransport}, "POST", req.URL())
    96  
    97  	// Create a new connection from the dialer.
    98  	//
    99  	// Warning: Any early return should close this connection, otherwise we're going to leak them.
   100  	connection, _, err := dialer.Dial(portforward.PortForwardProtocolV1Name)
   101  	if err != nil {
   102  		return nil, errors.Wrap(err, "error upgrading connection")
   103  	}
   104  
   105  	// Create the headers.
   106  	headers := http.Header{}
   107  
   108  	// Set the header port number to match the proxy one.
   109  	headers.Set(corev1.PortHeader, fmt.Sprintf("%d", d.proxy.Port))
   110  
   111  	// We only create a single stream over the connection
   112  	headers.Set(corev1.PortForwardRequestIDHeader, "0")
   113  
   114  	// Create the error stream.
   115  	headers.Set(corev1.StreamType, corev1.StreamTypeError)
   116  	errorStream, err := connection.CreateStream(headers)
   117  	if err != nil {
   118  		return nil, kerrors.NewAggregate([]error{
   119  			err,
   120  			connection.Close(),
   121  		})
   122  	}
   123  	// Close the error stream right away, we're not writing to it.
   124  	if err := errorStream.Close(); err != nil {
   125  		return nil, kerrors.NewAggregate([]error{
   126  			err,
   127  			connection.Close(),
   128  		})
   129  	}
   130  
   131  	// Create the data stream.
   132  	//
   133  	// NOTE: Given that we're reusing the headers,
   134  	// we need to overwrite the stream type before creating it.
   135  	headers.Set(corev1.StreamType, corev1.StreamTypeData)
   136  	dataStream, err := connection.CreateStream(headers)
   137  	if err != nil {
   138  		return nil, kerrors.NewAggregate([]error{
   139  			errors.Wrap(err, "error creating forwarding stream"),
   140  			connection.Close(),
   141  		})
   142  	}
   143  
   144  	// Create the net.Conn and return.
   145  	return NewConn(connection, dataStream), nil
   146  }
   147  
   148  // DialTimeout sets the timeout.
   149  func DialTimeout(duration time.Duration) func(*Dialer) error {
   150  	return func(d *Dialer) error {
   151  		return d.setTimeout(duration)
   152  	}
   153  }
   154  
   155  func (d *Dialer) setTimeout(duration time.Duration) error {
   156  	d.timeout = duration
   157  	return nil
   158  }