github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/context/docker/load.go (about)

     1  package docker
     2  
     3  import (
     4  	"crypto/tls"
     5  	"crypto/x509"
     6  	"encoding/pem"
     7  	"net"
     8  	"net/http"
     9  	"time"
    10  
    11  	"github.com/docker/cli/cli/connhelper"
    12  	"github.com/docker/cli/cli/context"
    13  	"github.com/docker/cli/cli/context/store"
    14  	"github.com/docker/docker/client"
    15  	"github.com/docker/go-connections/tlsconfig"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // EndpointMeta is a typed wrapper around a context-store generic endpoint describing
    20  // a Docker Engine endpoint, without its tls config
    21  type EndpointMeta = context.EndpointMetaBase
    22  
    23  // Endpoint is a typed wrapper around a context-store generic endpoint describing
    24  // a Docker Engine endpoint, with its tls data
    25  type Endpoint struct {
    26  	EndpointMeta
    27  	TLSData *context.TLSData
    28  
    29  	// Deprecated: Use of encrypted TLS private keys has been deprecated, and
    30  	// will be removed in a future release. Golang has deprecated support for
    31  	// legacy PEM encryption (as specified in RFC 1423), as it is insecure by
    32  	// design (see https://go-review.googlesource.com/c/go/+/264159).
    33  	TLSPassword string
    34  }
    35  
    36  // WithTLSData loads TLS materials for the endpoint
    37  func WithTLSData(s store.Reader, contextName string, m EndpointMeta) (Endpoint, error) {
    38  	tlsData, err := context.LoadTLSData(s, contextName, DockerEndpoint)
    39  	if err != nil {
    40  		return Endpoint{}, err
    41  	}
    42  	return Endpoint{
    43  		EndpointMeta: m,
    44  		TLSData:      tlsData,
    45  	}, nil
    46  }
    47  
    48  // tlsConfig extracts a context docker endpoint TLS config
    49  func (c *Endpoint) tlsConfig() (*tls.Config, error) {
    50  	if c.TLSData == nil && !c.SkipTLSVerify {
    51  		// there is no specific tls config
    52  		return nil, nil
    53  	}
    54  	var tlsOpts []func(*tls.Config)
    55  	if c.TLSData != nil && c.TLSData.CA != nil {
    56  		certPool := x509.NewCertPool()
    57  		if !certPool.AppendCertsFromPEM(c.TLSData.CA) {
    58  			return nil, errors.New("failed to retrieve context tls info: ca.pem seems invalid")
    59  		}
    60  		tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
    61  			cfg.RootCAs = certPool
    62  		})
    63  	}
    64  	if c.TLSData != nil && c.TLSData.Key != nil && c.TLSData.Cert != nil {
    65  		keyBytes := c.TLSData.Key
    66  		pemBlock, _ := pem.Decode(keyBytes)
    67  		if pemBlock == nil {
    68  			return nil, errors.New("no valid private key found")
    69  		}
    70  		if x509.IsEncryptedPEMBlock(pemBlock) { //nolint:staticcheck // SA1019: x509.IsEncryptedPEMBlock is deprecated, and insecure by design
    71  			return nil, errors.New("private key is encrypted - support for encrypted private keys has been removed, see https://docs.docker.com/go/deprecated/")
    72  		}
    73  
    74  		x509cert, err := tls.X509KeyPair(c.TLSData.Cert, keyBytes)
    75  		if err != nil {
    76  			return nil, errors.Wrap(err, "failed to retrieve context tls info")
    77  		}
    78  		tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
    79  			cfg.Certificates = []tls.Certificate{x509cert}
    80  		})
    81  	}
    82  	if c.SkipTLSVerify {
    83  		tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
    84  			cfg.InsecureSkipVerify = true
    85  		})
    86  	}
    87  	return tlsconfig.ClientDefault(tlsOpts...), nil
    88  }
    89  
    90  // ClientOpts returns a slice of Client options to configure an API client with this endpoint
    91  func (c *Endpoint) ClientOpts() ([]client.Opt, error) {
    92  	var result []client.Opt
    93  	if c.Host != "" {
    94  		helper, err := connhelper.GetConnectionHelper(c.Host)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		if helper == nil {
    99  			tlsConfig, err := c.tlsConfig()
   100  			if err != nil {
   101  				return nil, err
   102  			}
   103  			result = append(result,
   104  				withHTTPClient(tlsConfig),
   105  				client.WithHost(c.Host),
   106  			)
   107  
   108  		} else {
   109  			httpClient := &http.Client{
   110  				// No tls
   111  				// No proxy
   112  				Transport: &http.Transport{
   113  					DialContext: helper.Dialer,
   114  				},
   115  			}
   116  			result = append(result,
   117  				client.WithHTTPClient(httpClient),
   118  				client.WithHost(helper.Host),
   119  				client.WithDialContext(helper.Dialer),
   120  			)
   121  		}
   122  	}
   123  
   124  	result = append(result, client.WithVersionFromEnv(), client.WithAPIVersionNegotiation())
   125  	return result, nil
   126  }
   127  
   128  func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error {
   129  	return func(c *client.Client) error {
   130  		if tlsConfig == nil {
   131  			// Use the default HTTPClient
   132  			return nil
   133  		}
   134  
   135  		httpClient := &http.Client{
   136  			Transport: &http.Transport{
   137  				TLSClientConfig: tlsConfig,
   138  				DialContext: (&net.Dialer{
   139  					KeepAlive: 30 * time.Second,
   140  					Timeout:   30 * time.Second,
   141  				}).DialContext,
   142  			},
   143  			CheckRedirect: client.CheckRedirect,
   144  		}
   145  		return client.WithHTTPClient(httpClient)(c)
   146  	}
   147  }
   148  
   149  // EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure
   150  func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) {
   151  	ep, ok := metadata.Endpoints[DockerEndpoint]
   152  	if !ok {
   153  		return EndpointMeta{}, errors.New("cannot find docker endpoint in context")
   154  	}
   155  	typed, ok := ep.(EndpointMeta)
   156  	if !ok {
   157  		return EndpointMeta{}, errors.Errorf("endpoint %q is not of type EndpointMeta", DockerEndpoint)
   158  	}
   159  	return typed, nil
   160  }