package plugin import ( "context" "crypto/tls" "net" "net/http" "time" "github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes/docker" "github.com/containerd/log" "github.com/distribution/reference" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/dockerversion" "github.com/pkg/errors" ) // scope builds the correct auth scope for the registry client to authorize against // By default the client currently only does a "repository:" scope with out a classifier, e.g. "(plugin)" // Without this, the client will not be able to authorize the request func scope(ref reference.Named, push bool) string { scope := "repository(plugin):" + reference.Path(reference.TrimNamed(ref)) + ":pull" if push { scope += ",push" } return scope } func (pm *Manager) newResolver(ctx context.Context, tracker docker.StatusTracker, auth *registry.AuthConfig, headers http.Header, httpFallback bool) (remotes.Resolver, error) { if headers == nil { headers = http.Header{} } headers.Add("User-Agent", dockerversion.DockerUserAgent(ctx)) return docker.NewResolver(docker.ResolverOptions{ Tracker: tracker, Headers: headers, Hosts: pm.registryHostsFn(auth, httpFallback), }), nil } func registryHTTPClient(config *tls.Config) *http.Client { return &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, TLSClientConfig: config, TLSHandshakeTimeout: 10 * time.Second, IdleConnTimeout: 30 * time.Second, }, } } func (pm *Manager) registryHostsFn(auth *registry.AuthConfig, httpFallback bool) docker.RegistryHosts { return func(hostname string) ([]docker.RegistryHost, error) { eps, err := pm.config.RegistryService.LookupPullEndpoints(hostname) if err != nil { return nil, errors.Wrapf(err, "error resolving repository for %s", hostname) } hosts := make([]docker.RegistryHost, 0, len(eps)) for _, ep := range eps { // forced http fallback is used only for push since the containerd pusher only ever uses the first host we // pass to it. // So it is the callers responsibility to retry with this flag set. if httpFallback && ep.URL.Scheme != "http" { log.G(context.TODO()).WithField("registryHost", hostname).WithField("endpoint", ep).Debugf("Skipping non-http endpoint") continue } caps := docker.HostCapabilityPull | docker.HostCapabilityResolve if !ep.Mirror { caps = caps | docker.HostCapabilityPush } host, err := docker.DefaultHost(ep.URL.Host) if err != nil { return nil, err } client := registryHTTPClient(ep.TLSConfig) hosts = append(hosts, docker.RegistryHost{ Host: host, Scheme: ep.URL.Scheme, Client: client, Path: "/v2", Capabilities: caps, Authorizer: docker.NewDockerAuthorizer( docker.WithAuthClient(client), docker.WithAuthCreds(func(_ string) (string, string, error) { if auth.IdentityToken != "" { return "", auth.IdentityToken, nil } return auth.Username, auth.Password, nil }), ), }) } log.G(context.TODO()).WithField("registryHost", hostname).WithField("hosts", hosts).Debug("Resolved registry hosts") return hosts, nil } }