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 }