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 }