github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/remotes/docker/config/hosts.go (about)

     1  /*
     2     Copyright The containerd 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  // config package containers utilities for helping configure the Docker resolver
    18  package config
    19  
    20  import (
    21  	"context"
    22  	"crypto/tls"
    23  	"io/ioutil"
    24  	"net"
    25  	"net/http"
    26  	"net/url"
    27  	"os"
    28  	"path"
    29  	"path/filepath"
    30  	"strings"
    31  	"time"
    32  
    33  	"github.com/BurntSushi/toml"
    34  	"github.com/containerd/containerd/errdefs"
    35  	"github.com/containerd/containerd/log"
    36  	"github.com/containerd/containerd/remotes/docker"
    37  	"github.com/pkg/errors"
    38  )
    39  
    40  type hostConfig struct {
    41  	scheme string
    42  	host   string
    43  	path   string
    44  
    45  	capabilities docker.HostCapabilities
    46  
    47  	caCerts     []string
    48  	clientPairs [][2]string
    49  	skipVerify  *bool
    50  
    51  	header http.Header
    52  
    53  	// TODO: API ("docker" or "oci")
    54  	// TODO: API Version ("v1", "v2")
    55  	// TODO: Add credential configuration (domain alias, username)
    56  }
    57  
    58  // HostOptions is used to configure registry hosts
    59  type HostOptions struct {
    60  	HostDir       func(string) (string, error)
    61  	Credentials   func(host string) (string, string, error)
    62  	DefaultTLS    *tls.Config
    63  	DefaultScheme string
    64  }
    65  
    66  // ConfigureHosts creates a registry hosts function from the provided
    67  // host creation options. The host directory can read hosts.toml or
    68  // certificate files laid out in the Docker specific layout.
    69  // If a `HostDir` function is not required, defaults are used.
    70  func ConfigureHosts(ctx context.Context, options HostOptions) docker.RegistryHosts {
    71  	return func(host string) ([]docker.RegistryHost, error) {
    72  		var hosts []hostConfig
    73  		if options.HostDir != nil {
    74  			dir, err := options.HostDir(host)
    75  			if err != nil && !errdefs.IsNotFound(err) {
    76  				return nil, err
    77  			}
    78  			if dir != "" {
    79  				log.G(ctx).WithField("dir", dir).Debug("loading host directory")
    80  				hosts, err = loadHostDir(ctx, dir)
    81  				if err != nil {
    82  					return nil, err
    83  				}
    84  			}
    85  
    86  		}
    87  
    88  		// If hosts was not set, add a default host
    89  		// NOTE: Check nil here and not empty, the host may be
    90  		// intentionally configured to not have any endpoints
    91  		if hosts == nil {
    92  			hosts = make([]hostConfig, 1)
    93  		}
    94  		if len(hosts) > 0 && hosts[len(hosts)-1].host == "" {
    95  			if host == "docker.io" {
    96  				hosts[len(hosts)-1].scheme = "https"
    97  				hosts[len(hosts)-1].host = "registry-1.docker.io"
    98  			} else {
    99  				hosts[len(hosts)-1].host = host
   100  				if options.DefaultScheme != "" {
   101  					hosts[len(hosts)-1].scheme = options.DefaultScheme
   102  				} else {
   103  					hosts[len(hosts)-1].scheme = "https"
   104  				}
   105  			}
   106  			hosts[len(hosts)-1].path = "/v2"
   107  			hosts[len(hosts)-1].capabilities = docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush
   108  		}
   109  
   110  		var defaultTLSConfig *tls.Config
   111  		if options.DefaultTLS != nil {
   112  			defaultTLSConfig = options.DefaultTLS
   113  		} else {
   114  			defaultTLSConfig = &tls.Config{}
   115  		}
   116  
   117  		defaultTransport := &http.Transport{
   118  			Proxy: http.ProxyFromEnvironment,
   119  			DialContext: (&net.Dialer{
   120  				Timeout:       30 * time.Second,
   121  				KeepAlive:     30 * time.Second,
   122  				FallbackDelay: 300 * time.Millisecond,
   123  			}).DialContext,
   124  			MaxIdleConns:          10,
   125  			IdleConnTimeout:       30 * time.Second,
   126  			TLSHandshakeTimeout:   10 * time.Second,
   127  			TLSClientConfig:       defaultTLSConfig,
   128  			ExpectContinueTimeout: 5 * time.Second,
   129  		}
   130  
   131  		client := &http.Client{
   132  			Transport: defaultTransport,
   133  		}
   134  
   135  		authOpts := []docker.AuthorizerOpt{docker.WithAuthClient(client)}
   136  		if options.Credentials != nil {
   137  			authOpts = append(authOpts, docker.WithAuthCreds(options.Credentials))
   138  		}
   139  		authorizer := docker.NewDockerAuthorizer(authOpts...)
   140  
   141  		rhosts := make([]docker.RegistryHost, len(hosts))
   142  		for i, host := range hosts {
   143  
   144  			rhosts[i].Scheme = host.scheme
   145  			rhosts[i].Host = host.host
   146  			rhosts[i].Path = host.path
   147  			rhosts[i].Capabilities = host.capabilities
   148  			rhosts[i].Header = host.header
   149  
   150  			if host.caCerts != nil || host.clientPairs != nil || host.skipVerify != nil {
   151  				tr := defaultTransport.Clone()
   152  				tlsConfig := tr.TLSClientConfig
   153  				if host.skipVerify != nil {
   154  					tlsConfig.InsecureSkipVerify = *host.skipVerify
   155  				}
   156  				if host.caCerts != nil {
   157  					if tlsConfig.RootCAs == nil {
   158  						rootPool, err := rootSystemPool()
   159  						if err != nil {
   160  							return nil, errors.Wrap(err, "unable to initialize cert pool")
   161  						}
   162  						tlsConfig.RootCAs = rootPool
   163  					}
   164  					for _, f := range host.caCerts {
   165  						data, err := ioutil.ReadFile(f)
   166  						if err != nil {
   167  							return nil, errors.Wrapf(err, "unable to read CA cert %q", f)
   168  						}
   169  						if !tlsConfig.RootCAs.AppendCertsFromPEM(data) {
   170  							return nil, errors.Errorf("unable to load CA cert %q", f)
   171  						}
   172  					}
   173  				}
   174  
   175  				if host.clientPairs != nil {
   176  					for _, pair := range host.clientPairs {
   177  						certPEMBlock, err := ioutil.ReadFile(pair[0])
   178  						if err != nil {
   179  							return nil, errors.Wrapf(err, "unable to read CERT file %q", pair[0])
   180  						}
   181  						var keyPEMBlock []byte
   182  						if pair[1] != "" {
   183  							keyPEMBlock, err = ioutil.ReadFile(pair[1])
   184  							if err != nil {
   185  								return nil, errors.Wrapf(err, "unable to read CERT file %q", pair[1])
   186  							}
   187  						} else {
   188  							// Load key block from same PEM file
   189  							keyPEMBlock = certPEMBlock
   190  						}
   191  						cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
   192  						if err != nil {
   193  							return nil, errors.Wrap(err, "failed to load X509 key pair")
   194  						}
   195  
   196  						tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
   197  					}
   198  				}
   199  
   200  				c := *client
   201  				c.Transport = tr
   202  
   203  				rhosts[i].Client = &c
   204  				rhosts[i].Authorizer = docker.NewDockerAuthorizer(append(authOpts, docker.WithAuthClient(&c))...)
   205  			} else {
   206  				rhosts[i].Client = client
   207  				rhosts[i].Authorizer = authorizer
   208  			}
   209  		}
   210  
   211  		return rhosts, nil
   212  	}
   213  
   214  }
   215  
   216  // HostDirFromRoot returns a function which finds a host directory
   217  // based at the given root.
   218  func HostDirFromRoot(root string) func(string) (string, error) {
   219  	return func(host string) (string, error) {
   220  		for _, p := range hostPaths(root, host) {
   221  			if _, err := os.Stat(p); err == nil {
   222  				return p, nil
   223  			} else if !os.IsNotExist(err) {
   224  				return "", err
   225  			}
   226  		}
   227  		return "", errdefs.ErrNotFound
   228  	}
   229  }
   230  
   231  // hostDirectory converts ":port" to "_port_" in directory names
   232  func hostDirectory(host string) string {
   233  	idx := strings.LastIndex(host, ":")
   234  	if idx > 0 {
   235  		return host[:idx] + "_" + host[idx+1:] + "_"
   236  	}
   237  	return host
   238  }
   239  
   240  func loadHostDir(ctx context.Context, hostsDir string) ([]hostConfig, error) {
   241  	b, err := ioutil.ReadFile(filepath.Join(hostsDir, "hosts.toml"))
   242  	if err != nil && !os.IsNotExist(err) {
   243  		return nil, err
   244  	}
   245  
   246  	if len(b) == 0 {
   247  		// If hosts.toml does not exist, fallback to checking for
   248  		// certificate files based on Docker's certificate file
   249  		// pattern (".crt", ".cert", ".key" files)
   250  		return loadCertFiles(ctx, hostsDir)
   251  	}
   252  
   253  	hosts, err := parseHostsFile(ctx, hostsDir, b)
   254  	if err != nil {
   255  		log.G(ctx).WithError(err).Error("failed to decode hosts.toml")
   256  		// Fallback to checking certificate files
   257  		return loadCertFiles(ctx, hostsDir)
   258  	}
   259  
   260  	return hosts, nil
   261  }
   262  
   263  type hostFileConfig struct {
   264  	// Capabilities determine what operations a host is
   265  	// capable of performing. Allowed values
   266  	//  - pull
   267  	//  - resolve
   268  	//  - push
   269  	Capabilities []string `toml:"capabilities"`
   270  
   271  	// CACert can be a string or an array of strings
   272  	CACert toml.Primitive `toml:"ca"`
   273  
   274  	// TODO: Make this an array (two key types, one for pairs (multiple files), one for single file?)
   275  	Client toml.Primitive `toml:"client"`
   276  
   277  	SkipVerify *bool `toml:"skip_verify"`
   278  
   279  	Header map[string]toml.Primitive `toml:"header"`
   280  
   281  	// API (default: "docker")
   282  	// API Version (default: "v2")
   283  	// Credentials: helper? name? username? alternate domain? token?
   284  }
   285  
   286  type configFile struct {
   287  	// hostConfig holds defaults for all hosts as well as
   288  	// for the default server
   289  	hostFileConfig
   290  
   291  	// Server specifies the default server. When `host` is
   292  	// also specified, those hosts are tried first.
   293  	Server string `toml:"server"`
   294  
   295  	// HostConfigs store the per-host configuration
   296  	HostConfigs map[string]hostFileConfig `toml:"host"`
   297  }
   298  
   299  func parseHostsFile(ctx context.Context, baseDir string, b []byte) ([]hostConfig, error) {
   300  	var c configFile
   301  	md, err := toml.Decode(string(b), &c)
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  
   306  	var orderedHosts []string
   307  	for _, key := range md.Keys() {
   308  		if len(key) >= 2 {
   309  			if key[0] == "host" && (len(orderedHosts) == 0 || orderedHosts[len(orderedHosts)-1] != key[1]) {
   310  				orderedHosts = append(orderedHosts, key[1])
   311  			}
   312  		}
   313  	}
   314  
   315  	if c.HostConfigs == nil {
   316  		c.HostConfigs = map[string]hostFileConfig{}
   317  	}
   318  	if c.Server != "" {
   319  		c.HostConfigs[c.Server] = c.hostFileConfig
   320  		orderedHosts = append(orderedHosts, c.Server)
   321  	} else if len(orderedHosts) == 0 {
   322  		c.HostConfigs[""] = c.hostFileConfig
   323  		orderedHosts = append(orderedHosts, "")
   324  	}
   325  	hosts := make([]hostConfig, len(orderedHosts))
   326  	for i, server := range orderedHosts {
   327  		hostConfig := c.HostConfigs[server]
   328  
   329  		if server != "" {
   330  			if !strings.HasPrefix(server, "http") {
   331  				server = "https://" + server
   332  			}
   333  			u, err := url.Parse(server)
   334  			if err != nil {
   335  				return nil, errors.Errorf("unable to parse server %v", server)
   336  			}
   337  			hosts[i].scheme = u.Scheme
   338  			hosts[i].host = u.Host
   339  
   340  			// TODO: Handle path based on registry protocol
   341  			// Define a registry protocol type
   342  			//   OCI v1    - Always use given path as is
   343  			//   Docker v2 - Always ensure ends with /v2/
   344  			if len(u.Path) > 0 {
   345  				u.Path = path.Clean(u.Path)
   346  				if !strings.HasSuffix(u.Path, "/v2") {
   347  					u.Path = u.Path + "/v2"
   348  				}
   349  			} else {
   350  				u.Path = "/v2"
   351  			}
   352  			hosts[i].path = u.Path
   353  		}
   354  		hosts[i].skipVerify = hostConfig.SkipVerify
   355  
   356  		if len(hostConfig.Capabilities) > 0 {
   357  			for _, c := range hostConfig.Capabilities {
   358  				switch strings.ToLower(c) {
   359  				case "pull":
   360  					hosts[i].capabilities |= docker.HostCapabilityPull
   361  				case "resolve":
   362  					hosts[i].capabilities |= docker.HostCapabilityResolve
   363  				case "push":
   364  					hosts[i].capabilities |= docker.HostCapabilityPush
   365  				default:
   366  					return nil, errors.Errorf("unknown capability %v", c)
   367  				}
   368  			}
   369  		} else {
   370  			hosts[i].capabilities = docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush
   371  		}
   372  
   373  		baseKey := []string{}
   374  		if server != "" && server != c.Server {
   375  			baseKey = append(baseKey, "host", server)
   376  		}
   377  		caKey := append(baseKey, "ca")
   378  		if md.IsDefined(caKey...) {
   379  			switch t := md.Type(caKey...); t {
   380  			case "String":
   381  				var caCert string
   382  				if err := md.PrimitiveDecode(hostConfig.CACert, &caCert); err != nil {
   383  					return nil, errors.Wrap(err, "failed to decode \"ca\"")
   384  				}
   385  				hosts[i].caCerts = []string{makeAbsPath(caCert, baseDir)}
   386  			case "Array":
   387  				var caCerts []string
   388  				if err := md.PrimitiveDecode(hostConfig.CACert, &caCerts); err != nil {
   389  					return nil, errors.Wrap(err, "failed to decode \"ca\"")
   390  				}
   391  				for i, p := range caCerts {
   392  					caCerts[i] = makeAbsPath(p, baseDir)
   393  				}
   394  
   395  				hosts[i].caCerts = caCerts
   396  			default:
   397  				return nil, errors.Errorf("invalid type %v for \"ca\"", t)
   398  			}
   399  		}
   400  
   401  		clientKey := append(baseKey, "client")
   402  		if md.IsDefined(clientKey...) {
   403  			switch t := md.Type(clientKey...); t {
   404  			case "String":
   405  				var clientCert string
   406  				if err := md.PrimitiveDecode(hostConfig.Client, &clientCert); err != nil {
   407  					return nil, errors.Wrap(err, "failed to decode \"ca\"")
   408  				}
   409  				hosts[i].clientPairs = [][2]string{{makeAbsPath(clientCert, baseDir), ""}}
   410  			case "Array":
   411  				var clientCerts []interface{}
   412  				if err := md.PrimitiveDecode(hostConfig.Client, &clientCerts); err != nil {
   413  					return nil, errors.Wrap(err, "failed to decode \"ca\"")
   414  				}
   415  				for _, pairs := range clientCerts {
   416  					switch p := pairs.(type) {
   417  					case string:
   418  						hosts[i].clientPairs = append(hosts[i].clientPairs, [2]string{makeAbsPath(p, baseDir), ""})
   419  					case []interface{}:
   420  						var pair [2]string
   421  						if len(p) > 2 {
   422  							return nil, errors.Errorf("invalid pair %v for \"client\"", p)
   423  						}
   424  						for pi, cp := range p {
   425  							s, ok := cp.(string)
   426  							if !ok {
   427  								return nil, errors.Errorf("invalid type %T for \"client\"", cp)
   428  							}
   429  							pair[pi] = makeAbsPath(s, baseDir)
   430  						}
   431  						hosts[i].clientPairs = append(hosts[i].clientPairs, pair)
   432  					default:
   433  						return nil, errors.Errorf("invalid type %T for \"client\"", p)
   434  					}
   435  				}
   436  			default:
   437  				return nil, errors.Errorf("invalid type %v for \"client\"", t)
   438  			}
   439  		}
   440  
   441  		headerKey := append(baseKey, "header")
   442  		if md.IsDefined(headerKey...) {
   443  			header := http.Header{}
   444  			for key, prim := range hostConfig.Header {
   445  				switch t := md.Type(append(headerKey, key)...); t {
   446  				case "String":
   447  					var value string
   448  					if err := md.PrimitiveDecode(prim, &value); err != nil {
   449  						return nil, errors.Wrapf(err, "failed to decode header %q", key)
   450  					}
   451  					header[key] = []string{value}
   452  				case "Array":
   453  					var value []string
   454  					if err := md.PrimitiveDecode(prim, &value); err != nil {
   455  						return nil, errors.Wrapf(err, "failed to decode header %q", key)
   456  					}
   457  
   458  					header[key] = value
   459  				default:
   460  					return nil, errors.Errorf("invalid type %v for header %q", t, key)
   461  				}
   462  			}
   463  			hosts[i].header = header
   464  		}
   465  	}
   466  
   467  	return hosts, nil
   468  }
   469  
   470  func makeAbsPath(p string, base string) string {
   471  	if filepath.IsAbs(p) {
   472  		return p
   473  	}
   474  	return filepath.Join(base, p)
   475  }
   476  
   477  // loadCertsDir loads certs from certsDir like "/etc/docker/certs.d" .
   478  // Compatible with Docker file layout
   479  // - files ending with ".crt" are treated as CA certificate files
   480  // - files ending with ".cert" are treated as client certificates, and
   481  //   files with the same name but ending with ".key" are treated as the
   482  //   corresponding private key.
   483  //   NOTE: If a ".key" file is missing, this function will just return
   484  //   the ".cert", which may contain the private key. If the ".cert" file
   485  //   does not contain the private key, the caller should detect and error.
   486  func loadCertFiles(ctx context.Context, certsDir string) ([]hostConfig, error) {
   487  	fs, err := ioutil.ReadDir(certsDir)
   488  	if err != nil && !os.IsNotExist(err) {
   489  		return nil, err
   490  	}
   491  	hosts := make([]hostConfig, 1)
   492  	for _, f := range fs {
   493  		if !f.IsDir() {
   494  			continue
   495  		}
   496  		if strings.HasSuffix(f.Name(), ".crt") {
   497  			hosts[0].caCerts = append(hosts[0].caCerts, filepath.Join(certsDir, f.Name()))
   498  		}
   499  		if strings.HasSuffix(f.Name(), ".cert") {
   500  			var pair [2]string
   501  			certFile := f.Name()
   502  			pair[0] = filepath.Join(certsDir, certFile)
   503  			// Check if key also exists
   504  			keyFile := certFile[:len(certFile)-5] + ".key"
   505  			if _, err := os.Stat(keyFile); err == nil {
   506  				pair[1] = filepath.Join(certsDir, keyFile)
   507  			} else if !os.IsNotExist(err) {
   508  				return nil, err
   509  			}
   510  			hosts[0].clientPairs = append(hosts[0].clientPairs, pair)
   511  		}
   512  	}
   513  	return hosts, nil
   514  }