github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.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  package dockerconfigresolver
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  
    26  	"github.com/containerd/containerd/remotes"
    27  	"github.com/containerd/containerd/remotes/docker"
    28  	dockerconfig "github.com/containerd/containerd/remotes/docker/config"
    29  	"github.com/containerd/log"
    30  	dockercliconfig "github.com/docker/cli/cli/config"
    31  	"github.com/docker/cli/cli/config/credentials"
    32  	dockercliconfigtypes "github.com/docker/cli/cli/config/types"
    33  	"github.com/docker/docker/errdefs"
    34  )
    35  
    36  var PushTracker = docker.NewInMemoryTracker()
    37  
    38  type opts struct {
    39  	plainHTTP       bool
    40  	skipVerifyCerts bool
    41  	hostsDirs       []string
    42  	authCreds       AuthCreds
    43  }
    44  
    45  // Opt for New
    46  type Opt func(*opts)
    47  
    48  // WithPlainHTTP enables insecure plain HTTP
    49  func WithPlainHTTP(b bool) Opt {
    50  	return func(o *opts) {
    51  		o.plainHTTP = b
    52  	}
    53  }
    54  
    55  // WithSkipVerifyCerts skips verifying TLS certs
    56  func WithSkipVerifyCerts(b bool) Opt {
    57  	return func(o *opts) {
    58  		o.skipVerifyCerts = b
    59  	}
    60  }
    61  
    62  // WithHostsDirs specifies directories like /etc/containerd/certs.d and /etc/docker/certs.d
    63  func WithHostsDirs(orig []string) Opt {
    64  	var ss []string
    65  	if len(orig) == 0 {
    66  		log.L.Debug("no hosts dir was specified")
    67  	}
    68  	for _, v := range orig {
    69  		if _, err := os.Stat(v); err == nil {
    70  			log.L.Debugf("Found hosts dir %q", v)
    71  			ss = append(ss, v)
    72  		} else {
    73  			if errors.Is(err, os.ErrNotExist) {
    74  				log.L.WithError(err).Debugf("Ignoring hosts dir %q", v)
    75  			} else {
    76  				log.L.WithError(err).Warnf("Ignoring hosts dir %q", v)
    77  			}
    78  		}
    79  	}
    80  	return func(o *opts) {
    81  		o.hostsDirs = ss
    82  	}
    83  }
    84  
    85  func WithAuthCreds(ac AuthCreds) Opt {
    86  	return func(o *opts) {
    87  		o.authCreds = ac
    88  	}
    89  }
    90  
    91  // NewHostOptions instantiates a HostOptions struct using $DOCKER_CONFIG/config.json .
    92  //
    93  // $DOCKER_CONFIG defaults to "~/.docker".
    94  //
    95  // refHostname is like "docker.io".
    96  func NewHostOptions(ctx context.Context, refHostname string, optFuncs ...Opt) (*dockerconfig.HostOptions, error) {
    97  	var o opts
    98  	for _, of := range optFuncs {
    99  		of(&o)
   100  	}
   101  	var ho dockerconfig.HostOptions
   102  
   103  	ho.HostDir = func(s string) (string, error) {
   104  		for _, hostsDir := range o.hostsDirs {
   105  			found, err := dockerconfig.HostDirFromRoot(hostsDir)(s)
   106  			if (err != nil && !errdefs.IsNotFound(err)) || (found != "") {
   107  				return found, err
   108  			}
   109  		}
   110  		return "", nil
   111  	}
   112  
   113  	if o.authCreds != nil {
   114  		ho.Credentials = o.authCreds
   115  	} else {
   116  		authCreds, err := NewAuthCreds(refHostname)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  		ho.Credentials = authCreds
   121  
   122  	}
   123  
   124  	if o.skipVerifyCerts {
   125  		ho.DefaultTLS = &tls.Config{
   126  			InsecureSkipVerify: true,
   127  		}
   128  	}
   129  
   130  	if o.plainHTTP {
   131  		ho.DefaultScheme = "http"
   132  	} else {
   133  		if isLocalHost, err := docker.MatchLocalhost(refHostname); err != nil {
   134  			return nil, err
   135  		} else if isLocalHost {
   136  			ho.DefaultScheme = "http"
   137  		}
   138  	}
   139  	if ho.DefaultScheme == "http" {
   140  		// https://github.com/containerd/containerd/issues/9208
   141  		ho.DefaultTLS = nil
   142  	}
   143  	return &ho, nil
   144  }
   145  
   146  // New instantiates a resolver using $DOCKER_CONFIG/config.json .
   147  //
   148  // $DOCKER_CONFIG defaults to "~/.docker".
   149  //
   150  // refHostname is like "docker.io".
   151  func New(ctx context.Context, refHostname string, optFuncs ...Opt) (remotes.Resolver, error) {
   152  	ho, err := NewHostOptions(ctx, refHostname, optFuncs...)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	resolverOpts := docker.ResolverOptions{
   158  		Tracker: PushTracker,
   159  		Hosts:   dockerconfig.ConfigureHosts(ctx, *ho),
   160  	}
   161  
   162  	resolver := docker.NewResolver(resolverOpts)
   163  	return resolver, nil
   164  }
   165  
   166  // AuthCreds is for docker.WithAuthCreds
   167  type AuthCreds func(string) (string, string, error)
   168  
   169  // NewAuthCreds returns AuthCreds that uses $DOCKER_CONFIG/config.json .
   170  // AuthCreds can be nil.
   171  func NewAuthCreds(refHostname string) (AuthCreds, error) {
   172  	// Load does not raise an error on ENOENT
   173  	dockerConfigFile, err := dockercliconfig.Load("")
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	// DefaultHost converts "docker.io" to "registry-1.docker.io",
   179  	// which is wanted  by credFunc .
   180  	credFuncExpectedHostname, err := docker.DefaultHost(refHostname)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	var credFunc AuthCreds
   186  
   187  	authConfigHostnames := []string{refHostname}
   188  	if refHostname == "docker.io" || refHostname == "registry-1.docker.io" {
   189  		// "docker.io" appears as ""https://index.docker.io/v1/" in ~/.docker/config.json .
   190  		// Unlike other registries, we have to pass the full URL to GetAuthConfig.
   191  		authConfigHostnames = append([]string{IndexServer}, refHostname)
   192  	}
   193  
   194  	for _, authConfigHostname := range authConfigHostnames {
   195  		// GetAuthConfig does not raise an error on ENOENT
   196  		ac, err := dockerConfigFile.GetAuthConfig(authConfigHostname)
   197  		if err != nil {
   198  			log.L.WithError(err).Warnf("cannot get auth config for authConfigHostname=%q (refHostname=%q)",
   199  				authConfigHostname, refHostname)
   200  		} else {
   201  			// When refHostname is "docker.io":
   202  			// - credFuncExpectedHostname: "registry-1.docker.io"
   203  			// - credFuncArg:              "registry-1.docker.io"
   204  			// - authConfigHostname:       "https://index.docker.io/v1/" (IndexServer)
   205  			// - ac.ServerAddress:         "https://index.docker.io/v1/".
   206  			if !isAuthConfigEmpty(ac) {
   207  				if ac.ServerAddress == "" {
   208  					// This can happen with Amazon ECR: https://github.com/containerd/nerdctl/issues/733
   209  					log.L.Debugf("failed to get ac.ServerAddress for authConfigHostname=%q (refHostname=%q)",
   210  						authConfigHostname, refHostname)
   211  				} else if authConfigHostname == IndexServer {
   212  					if ac.ServerAddress != IndexServer {
   213  						return nil, fmt.Errorf("expected ac.ServerAddress (%q) to be %q", ac.ServerAddress, IndexServer)
   214  					}
   215  				} else {
   216  					acsaHostname := credentials.ConvertToHostname(ac.ServerAddress)
   217  					if acsaHostname != authConfigHostname {
   218  						return nil, fmt.Errorf("expected the hostname part of ac.ServerAddress (%q) to be authConfigHostname=%q, got %q",
   219  							ac.ServerAddress, authConfigHostname, acsaHostname)
   220  					}
   221  				}
   222  
   223  				if ac.RegistryToken != "" {
   224  					// Even containerd/CRI does not support RegistryToken as of v1.4.3,
   225  					// so, nobody is actually using RegistryToken?
   226  					log.L.Warnf("ac.RegistryToken (for %q) is not supported yet (FIXME)", authConfigHostname)
   227  				}
   228  
   229  				credFunc = func(credFuncArg string) (string, string, error) {
   230  					// credFuncArg should be like "registry-1.docker.io"
   231  					if credFuncArg != credFuncExpectedHostname {
   232  						return "", "", fmt.Errorf("expected credFuncExpectedHostname=%q (refHostname=%q), got credFuncArg=%q",
   233  							credFuncExpectedHostname, refHostname, credFuncArg)
   234  					}
   235  					if ac.IdentityToken != "" {
   236  						return "", ac.IdentityToken, nil
   237  					}
   238  					return ac.Username, ac.Password, nil
   239  				}
   240  				break
   241  			}
   242  		}
   243  	}
   244  	// credsFunc can be nil here
   245  	return credFunc, nil
   246  }
   247  
   248  func isAuthConfigEmpty(ac dockercliconfigtypes.AuthConfig) bool {
   249  	if ac.IdentityToken != "" || ac.Username != "" || ac.Password != "" || ac.RegistryToken != "" {
   250  		return false
   251  	}
   252  	return true
   253  }