Merge pull request #6207 from thaJeztah/fork_registry

add internal fork of docker/docker/registry
This commit is contained in:
Sebastiaan van Stijn
2025-07-25 12:57:47 +02:00
committed by GitHub
33 changed files with 695 additions and 1165 deletions

View File

@ -16,8 +16,8 @@ import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/internal/registry"
"github.com/docker/cli/internal/tui"
"github.com/docker/docker/registry"
"github.com/moby/moby/api/types/auxprogress"
"github.com/moby/moby/api/types/image"
registrytypes "github.com/moby/moby/api/types/registry"

View File

@ -11,7 +11,7 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/docker/registry"
"github.com/docker/cli/internal/registry"
"github.com/moby/moby/api/types/image"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/opencontainers/go-digest"

View File

@ -11,7 +11,7 @@ import (
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/cli/internal/prompt"
"github.com/docker/docker/registry"
"github.com/docker/cli/internal/registry"
"github.com/moby/moby/api/types"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/pkg/errors"

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/docker/registry"
"github.com/docker/cli/internal/registry"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/pkg/errors"
"github.com/spf13/cobra"

View File

@ -15,8 +15,8 @@ import (
"github.com/docker/cli/cli/config/configfile"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/internal/oauth/manager"
"github.com/docker/cli/internal/registry"
"github.com/docker/cli/internal/tui"
"github.com/docker/docker/registry"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
"github.com/pkg/errors"
@ -288,7 +288,7 @@ func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (*regis
return nil, err
}
_, token, err := svc.Auth(ctx, &auth, command.UserAgent())
token, err := svc.Auth(ctx, &auth, command.UserAgent())
if err != nil {
return nil, err
}

View File

@ -13,8 +13,8 @@ import (
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/internal/registry"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/registry"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/internal/oauth/manager"
"github.com/docker/docker/registry"
"github.com/docker/cli/internal/registry"
"github.com/spf13/cobra"
)

View File

@ -6,7 +6,7 @@ import (
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/docker/registry"
"github.com/docker/cli/internal/registry"
"github.com/moby/moby/api/types/swarm"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"

View File

@ -19,8 +19,8 @@ import (
"github.com/docker/cli/cli/debug"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/internal/lazyregexp"
"github.com/docker/cli/internal/registry"
"github.com/docker/cli/templates"
"github.com/docker/docker/registry"
"github.com/docker/go-units"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/system"

View File

@ -78,7 +78,6 @@ var sampleInfoNoSwarm = system.Info{
IndexConfigs: map[string]*registrytypes.IndexInfo{
"docker.io": {
Name: "docker.io",
Mirrors: nil,
Secure: true,
Official: true,
},

View File

@ -1,14 +1,16 @@
package client
import (
"context"
"net"
"net/http"
"net/url"
"time"
"github.com/distribution/reference"
"github.com/docker/cli/internal/registry"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/registry"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/pkg/errors"
)
@ -54,7 +56,7 @@ func getDefaultEndpoint(repoName reference.Named, insecure bool) (registry.APIEn
if err != nil {
return registry.APIEndpoint{}, err
}
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoName))
endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(repoName))
if err != nil {
return registry.APIEndpoint{}, err
}
@ -97,7 +99,7 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
if len(actions) == 0 {
actions = []string{"pull"}
}
creds := registry.NewStaticCredentialStore(&authConfig)
creds := &staticCredentialStore{authConfig: &authConfig}
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
@ -117,3 +119,23 @@ func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, _ map[string
func (*existingTokenHandler) Scheme() string {
return "bearer"
}
type staticCredentialStore struct {
authConfig *registrytypes.AuthConfig
}
func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
if scs.authConfig == nil {
return "", ""
}
return scs.authConfig.Username, scs.authConfig.Password
}
func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
if scs.authConfig == nil {
return ""
}
return scs.authConfig.IdentityToken
}
func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {}

View File

@ -6,6 +6,7 @@ import (
"github.com/distribution/reference"
"github.com/docker/cli/cli/manifest/types"
"github.com/docker/cli/internal/registry"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/ocischema"
@ -13,7 +14,6 @@ import (
"github.com/docker/distribution/registry/api/errcode"
v2 "github.com/docker/distribution/registry/api/v2"
distclient "github.com/docker/distribution/registry/client"
"github.com/docker/docker/registry"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
@ -283,10 +283,10 @@ func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoi
}
registryService, err := registry.NewService(serviceOpts)
if err != nil {
return []registry.APIEndpoint{}, err
return nil, err
}
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(repoInfo.Name))
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
return endpoints, err
}

View File

@ -14,10 +14,10 @@ import (
"github.com/distribution/reference"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/internal/registry"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/registry"
"github.com/docker/go-connections/tlsconfig"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/opencontainers/go-digest"

View File

@ -11,7 +11,7 @@ import (
"github.com/distribution/reference"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/jsonstream"
"github.com/docker/docker/registry"
"github.com/docker/cli/internal/registry"
"github.com/moby/moby/api/types"
registrytypes "github.com/moby/moby/api/types/registry"
"github.com/opencontainers/go-digest"

View File

@ -14,8 +14,8 @@ import (
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/oauth"
"github.com/docker/cli/internal/oauth/api"
"github.com/docker/cli/internal/registry"
"github.com/docker/cli/internal/tui"
"github.com/docker/docker/registry"
"github.com/morikuni/aec"
"github.com/sirupsen/logrus"

View File

@ -2,6 +2,7 @@ package registry
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
@ -12,7 +13,6 @@ import (
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
"github.com/moby/moby/api/types/registry"
"github.com/pkg/errors"
)
// AuthClientID is used the ClientID used for the token server
@ -34,35 +34,6 @@ func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token strin
lcs.authConfig.IdentityToken = token
}
type staticCredentialStore struct {
auth *registry.AuthConfig
}
// NewStaticCredentialStore returns a credential store
// which always returns the same credential values.
func NewStaticCredentialStore(ac *registry.AuthConfig) auth.CredentialStore {
return staticCredentialStore{
auth: ac,
}
}
func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
if scs.auth == nil {
return "", ""
}
return scs.auth.Username, scs.auth.Password
}
func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
if scs.auth == nil {
return ""
}
return scs.auth.IdentityToken
}
func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
}
// loginV2 tries to login to the v2 registry server. The given registry
// endpoint will be pinged to get authorization challenges. These challenges
// will be used to authenticate against the registry to validate credentials.
@ -96,7 +67,7 @@ func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIE
if resp.StatusCode != http.StatusOK {
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
return "", fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
}
return credentialAuthConfig.IdentityToken, nil
@ -127,67 +98,19 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi
}, nil
}
// ConvertToHostname normalizes a registry URL which has http|https prepended
// to just its hostname. It is used to match credentials, which may be either
// stored as hostname or as hostname including scheme (in legacy configuration
// files).
func ConvertToHostname(maybeURL string) string {
stripped := maybeURL
if scheme, remainder, ok := strings.Cut(stripped, "://"); ok {
switch scheme {
case "http", "https":
stripped = remainder
default:
// unknown, or no scheme; doing nothing for now, as we never did.
}
}
stripped, _, _ = strings.Cut(stripped, "/")
return stripped
}
// ResolveAuthConfig matches an auth configuration to a server address or a URL
func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig {
configKey := GetAuthConfigKey(index)
// First try the happy case
if c, found := authConfigs[configKey]; found || index.Official {
return c
}
// Maybe they have a legacy config file, we will iterate the keys converting
// them to the new format and testing
for registryURL, ac := range authConfigs {
if configKey == ConvertToHostname(registryURL) {
return ac
}
}
// When all else fails, return an empty auth config
return registry.AuthConfig{}
}
// PingResponseError is used when the response from a ping
// was received but invalid.
type PingResponseError struct {
Err error
}
func (err PingResponseError) Error() string {
return err.Err.Error()
}
// PingV2Registry attempts to ping a v2 registry and on success return a
// challenge manager for the supported authentication types.
// If a response is received but cannot be interpreted, a PingResponseError will be returned.
func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
pingClient := &http.Client{
Transport: authTransport,
Timeout: 15 * time.Second,
}
endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody)
if err != nil {
return nil, err
}
pingClient := &http.Client{
Transport: authTransport,
Timeout: 15 * time.Second,
}
resp, err := pingClient.Do(req)
if err != nil {
return nil, err
@ -196,9 +119,7 @@ func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challen
challengeManager := challenge.NewSimpleManager()
if err := challengeManager.AddResponse(resp); err != nil {
return nil, PingResponseError{
Err: err,
}
return nil, err
}
return challengeManager, nil

View File

@ -5,6 +5,7 @@ package registry
import (
"context"
"fmt"
"net"
"net/url"
"os"
@ -21,13 +22,19 @@ import (
)
// ServiceOptions holds command line options.
//
// TODO(thaJeztah): add CertsDir as option to replace the [CertsDir] function, which sets the location magically.
type ServiceOptions struct {
Mirrors []string `json:"registry-mirrors,omitempty"`
InsecureRegistries []string `json:"insecure-registries,omitempty"`
}
// serviceConfig holds daemon configuration for the registry service.
type serviceConfig registry.ServiceConfig
//
// It's a reduced version of [registry.ServiceConfig] for the CLI.
type serviceConfig struct {
insecureRegistryCIDRs []*net.IPNet
indexConfigs map[string]*registry.IndexInfo
}
// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains
// are here for historic reasons and backward-compatibility. These domains
@ -39,27 +46,22 @@ type serviceConfig registry.ServiceConfig
const (
// DefaultNamespace is the default namespace
DefaultNamespace = "docker.io"
// DefaultRegistryHost is the hostname for the default (Docker Hub) registry
// used for pushing and pulling images. This hostname is hard-coded to handle
// the conversion from image references without registry name (e.g. "ubuntu",
// or "ubuntu:latest"), as well as references using the "docker.io" domain
// name, which is used as canonical reference for images on Docker Hub, but
// does not match the domain-name of Docker Hub's registry.
DefaultRegistryHost = "registry-1.docker.io"
// IndexHostname is the index hostname, used for authentication and image search.
IndexHostname = "index.docker.io"
// IndexServer is used for user auth and image search
IndexServer = "https://" + IndexHostname + "/v1/"
IndexServer = "https://index.docker.io/v1/"
// IndexName is the name of the index
IndexName = "docker.io"
)
var (
// DefaultV2Registry is the URI of the default (Docker Hub) registry.
DefaultV2Registry = &url.URL{
Scheme: "https",
Host: DefaultRegistryHost,
}
// DefaultV2Registry is the URI of the default (Docker Hub) registry
// used for pushing and pulling images. This hostname is hard-coded to handle
// the conversion from image references without registry name (e.g. "ubuntu",
// or "ubuntu:latest"), as well as references using the "docker.io" domain
// name, which is used as canonical reference for images on Docker Hub, but
// does not match the domain-name of Docker Hub's registry.
DefaultV2Registry = &url.URL{Scheme: "https", Host: "registry-1.docker.io"}
validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
@ -93,81 +95,22 @@ func CertsDir() string {
return certsDir
}
// newServiceConfig returns a new instance of ServiceConfig
func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
config := &serviceConfig{}
if err := config.loadMirrors(options.Mirrors); err != nil {
return nil, err
// newServiceConfig creates a new service config with the given options.
func newServiceConfig(registries []string) (*serviceConfig, error) {
if len(registries) == 0 {
return &serviceConfig{}, nil
}
if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil {
return nil, err
}
return config, nil
}
// copy constructs a new ServiceConfig with a copy of the configuration in config.
func (config *serviceConfig) copy() *registry.ServiceConfig {
ic := make(map[string]*registry.IndexInfo)
for key, value := range config.IndexConfigs {
ic[key] = value
}
return &registry.ServiceConfig{
InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...),
IndexConfigs: ic,
Mirrors: append([]string(nil), config.Mirrors...),
}
}
// loadMirrors loads mirrors to config, after removing duplicates.
// Returns an error if mirrors contains an invalid mirror.
func (config *serviceConfig) loadMirrors(mirrors []string) error {
mMap := map[string]struct{}{}
unique := []string{}
for _, mirror := range mirrors {
m, err := ValidateMirror(mirror)
if err != nil {
return err
}
if _, exist := mMap[m]; !exist {
mMap[m] = struct{}{}
unique = append(unique, m)
}
}
config.Mirrors = unique
// Configure public registry since mirrors may have changed.
config.IndexConfigs = map[string]*registry.IndexInfo{
IndexName: {
Name: IndexName,
Mirrors: unique,
Secure: true,
Official: true,
},
}
return nil
}
// loadInsecureRegistries loads insecure registries to config
func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
// Localhost is by default considered as an insecure registry. This is a
// stop-gap for people who are running a private registry on localhost.
registries = append(registries, "::1/128", "127.0.0.0/8")
var (
insecureRegistryCIDRs = make([]*registry.NetIPNet, 0)
insecureRegistryCIDRs = make([]*net.IPNet, 0)
indexConfigs = make(map[string]*registry.IndexInfo)
)
skip:
for _, r := range registries {
// validate insecure registry
if _, err := ValidateIndexName(r); err != nil {
return err
}
if scheme, host, ok := strings.Cut(r, "://"); ok {
switch strings.ToLower(scheme) {
case "http", "https":
@ -175,29 +118,27 @@ skip:
r = host
default:
// unsupported scheme
return invalidParamf("insecure registry %s should not contain '://'", r)
return nil, invalidParam(fmt.Errorf("insecure registry %s should not contain '://'", r))
}
}
// Check if CIDR was passed to --insecure-registry
_, ipnet, err := net.ParseCIDR(r)
if err == nil {
// Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip.
data := (*registry.NetIPNet)(ipnet)
for _, value := range insecureRegistryCIDRs {
if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
if value.IP.String() == ipnet.IP.String() && value.Mask.String() == ipnet.Mask.String() {
continue skip
}
}
// ipnet is not found, add it in config.InsecureRegistryCIDRs
insecureRegistryCIDRs = append(insecureRegistryCIDRs, data)
insecureRegistryCIDRs = append(insecureRegistryCIDRs, ipnet)
} else {
if err := validateHostPort(r); err != nil {
return invalidParamWrapf(err, "insecure registry %s is not valid", r)
return nil, invalidParam(fmt.Errorf("insecure registry %s is not valid: %w", r, err))
}
// Assume `host:port` if not CIDR.
indexConfigs[r] = &registry.IndexInfo{
Name: r,
Mirrors: []string{},
Secure: false,
Official: false,
}
@ -207,14 +148,14 @@ skip:
// Configure public registry.
indexConfigs[IndexName] = &registry.IndexInfo{
Name: IndexName,
Mirrors: config.Mirrors,
Secure: true,
Official: true,
}
config.InsecureRegistryCIDRs = insecureRegistryCIDRs
config.IndexConfigs = indexConfigs
return nil
return &serviceConfig{
indexConfigs: indexConfigs,
insecureRegistryCIDRs: insecureRegistryCIDRs,
}, nil
}
// isSecureIndex returns false if the provided indexName is part of the list of insecure registries
@ -231,11 +172,11 @@ skip:
func (config *serviceConfig) isSecureIndex(indexName string) bool {
// Check for configured index, first. This is needed in case isSecureIndex
// is called from anything besides newIndexInfo, in order to honor per-index configurations.
if index, ok := config.IndexConfigs[indexName]; ok {
if index, ok := config.indexConfigs[indexName]; ok {
return index.Secure
}
return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName)
return !isCIDRMatch(config.insecureRegistryCIDRs, indexName)
}
// for mocking in unit tests.
@ -244,7 +185,7 @@ var lookupIP = net.LookupIP
// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host")
// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be
// resolved to IP addresses for matching. If resolution fails, false is returned.
func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
func isCIDRMatch(cidrs []*net.IPNet, urlHost string) bool {
if len(cidrs) == 0 {
return false
}
@ -271,7 +212,7 @@ func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
for _, addr := range addresses {
for _, ipnet := range cidrs {
// check if the addr falls in the subnet
if (*net.IPNet)(ipnet).Contains(addr) {
if ipnet.Contains(addr) {
return true
}
}
@ -280,58 +221,13 @@ func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
return false
}
// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It
// returns an error if the given mirrorURL is invalid, or the normalized
// format for the URL otherwise.
//
// It is used by the daemon to validate the daemon configuration.
func ValidateMirror(mirrorURL string) (string, error) {
// Fast path for missing scheme, as url.Parse splits by ":", which can
// cause the hostname to be considered the "scheme" when using "hostname:port".
if scheme, _, ok := strings.Cut(mirrorURL, "://"); !ok || scheme == "" {
return "", invalidParamf("invalid mirror: no scheme specified for %q: must use either 'https://' or 'http://'", mirrorURL)
}
uri, err := url.Parse(mirrorURL)
if err != nil {
return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL)
}
if uri.Scheme != "http" && uri.Scheme != "https" {
return "", invalidParamf("invalid mirror: unsupported scheme %q in %q: must use either 'https://' or 'http://'", uri.Scheme, uri)
}
if uri.RawQuery != "" || uri.Fragment != "" {
return "", invalidParamf("invalid mirror: query or fragment at end of the URI %q", uri)
}
if uri.User != nil {
// strip password from output
uri.User = url.UserPassword(uri.User.Username(), "xxxxx")
return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri)
}
return strings.TrimSuffix(mirrorURL, "/") + "/", nil
}
// ValidateIndexName validates an index name. It is used by the daemon to
// validate the daemon configuration.
func ValidateIndexName(val string) (string, error) {
val = normalizeIndexName(val)
if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val)
}
return val, nil
}
func normalizeIndexName(val string) string {
// TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/".
// TODO: upstream this to check to reference package
if val == "index.docker.io" {
return "docker.io"
}
return val
}
func hasScheme(reposName string) bool {
return strings.Contains(reposName, "://")
}
func validateHostPort(s string) error {
// Split host and port, and in case s can not be split, assume host only
host, port, err := net.SplitHostPort(s)
@ -356,32 +252,6 @@ func validateHostPort(s string) error {
return nil
}
// newIndexInfo returns IndexInfo configuration from indexName
func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
indexName = normalizeIndexName(indexName)
// Return any configured index info, first.
if index, ok := config.IndexConfigs[indexName]; ok {
return index
}
// Construct a non-configured index info.
return &registry.IndexInfo{
Name: indexName,
Mirrors: []string{},
Secure: config.isSecureIndex(indexName),
}
}
// GetAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
func GetAuthConfigKey(index *registry.IndexInfo) string {
if index.Official {
return IndexServer
}
return index.Name
}
// ParseRepositoryInfo performs the breakdown of a repository name into a
// [RepositoryInfo], but lacks registry configuration.
//
@ -393,7 +263,6 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
Name: reference.TrimNamed(reposName),
Index: &registry.IndexInfo{
Name: IndexName,
Mirrors: []string{},
Secure: true,
Official: true,
},
@ -403,9 +272,8 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
return &RepositoryInfo{
Name: reference.TrimNamed(reposName),
Index: &registry.IndexInfo{
Name: indexName,
Mirrors: []string{},
Secure: !isInsecure(indexName),
Name: indexName,
Secure: !isInsecure(indexName),
},
}, nil
}

View File

@ -0,0 +1,92 @@
package registry
import (
"testing"
cerrdefs "github.com/containerd/errdefs"
"gotest.tools/v3/assert"
)
func TestLoadInsecureRegistries(t *testing.T) {
testCases := []struct {
registries []string
index string
err string
}{
{
registries: []string{"127.0.0.1"},
index: "127.0.0.1",
},
{
registries: []string{"127.0.0.1:8080"},
index: "127.0.0.1:8080",
},
{
registries: []string{"2001:db8::1"},
index: "2001:db8::1",
},
{
registries: []string{"[2001:db8::1]:80"},
index: "[2001:db8::1]:80",
},
{
registries: []string{"http://myregistry.example.com"},
index: "myregistry.example.com",
},
{
registries: []string{"https://myregistry.example.com"},
index: "myregistry.example.com",
},
{
registries: []string{"HTTP://myregistry.example.com"},
index: "myregistry.example.com",
},
{
registries: []string{"svn://myregistry.example.com"},
err: "insecure registry svn://myregistry.example.com should not contain '://'",
},
{
registries: []string{`mytest-.com`},
err: `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`,
},
{
registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`},
err: `insecure registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`,
},
{
registries: []string{`myregistry.example.com:500000`},
err: `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`,
},
{
registries: []string{`"myregistry.example.com"`},
err: `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`,
},
{
registries: []string{`"myregistry.example.com:5000"`},
err: `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`,
},
}
for _, testCase := range testCases {
config, err := newServiceConfig(testCase.registries)
if testCase.err == "" {
if err != nil {
t.Fatalf("expect no error, got '%s'", err)
}
match := false
for index := range config.indexConfigs {
if index == testCase.index {
match = true
}
}
if !match {
t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.indexConfigs)
}
} else {
if err == nil {
t.Fatalf("expect error '%s', got no error", testCase.err)
}
assert.ErrorContains(t, err, testCase.err)
assert.Check(t, cerrdefs.IsInvalidArgument(err))
}
}
}

12
internal/registry/doc.go Normal file
View File

@ -0,0 +1,12 @@
// Package registry is a fork of [github.com/docker/docker/registry], taken
// at commit [moby@49306c6]. Git history was not preserved in this fork,
// but can be found using the URLs provided.
//
// This fork was created to remove the dependency on the "Moby" codebase,
// and because the CLI only needs a subset of its features. The original
// package was written specifically for use in the daemon code, and includes
// functionality that cannot be used in the CLI.
//
// [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry
// [moby@49306c6]: https://github.com/moby/moby/tree/49306c607b72c5bf0a8e426f5a9760fa5ef96ea0/registry
package registry

View File

@ -1,10 +1,14 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.23
package registry
import (
"errors"
"fmt"
"net/url"
"github.com/docker/distribution/registry/api/errcode"
"github.com/pkg/errors"
)
func translateV2AuthError(err error) error {
@ -22,12 +26,8 @@ func invalidParam(err error) error {
return invalidParameterErr{err}
}
func invalidParamf(format string, args ...interface{}) error {
return invalidParameterErr{errors.Errorf(format, args...)}
}
func invalidParamWrapf(err error, format string, args ...interface{}) error {
return invalidParameterErr{errors.Wrapf(err, format, args...)}
func invalidParamf(format string, args ...any) error {
return invalidParameterErr{fmt.Errorf(format, args...)}
}
type unauthorizedErr struct{ error }
@ -49,19 +49,3 @@ func (invalidParameterErr) InvalidParameter() {}
func (e invalidParameterErr) Unwrap() error {
return e.error
}
type systemErr struct{ error }
func (systemErr) System() {}
func (e systemErr) Unwrap() error {
return e.error
}
type errUnknown struct{ error }
func (errUnknown) Unknown() {}
func (e errUnknown) Unwrap() error {
return e.error
}

View File

@ -4,6 +4,7 @@ package registry
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
@ -82,7 +83,7 @@ func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config)
if tlsConfig.RootCAs == nil {
systemPool, err := tlsconfig.SystemCertPool()
if err != nil {
return invalidParamWrapf(err, "unable to get system cert pool")
return invalidParam(fmt.Errorf("unable to get system cert pool: %w", err))
}
tlsConfig.RootCAs = systemPool
}

View File

@ -0,0 +1,92 @@
package registry
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/containerd/log"
"github.com/moby/moby/api/types/registry"
"gotest.tools/v3/assert"
)
var testHTTPServer *httptest.Server
func init() {
r := http.NewServeMux()
// /v1/
r.HandleFunc("/v1/_ping", handlerGetPing)
r.HandleFunc("/v1/search", handlerSearch)
// /v2/
r.HandleFunc("/v2/version", handlerGetPing)
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
}
func handlerAccessLog(handler http.Handler) http.Handler {
logHandler := func(w http.ResponseWriter, r *http.Request) {
log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL)
handler.ServeHTTP(w, r)
}
return http.HandlerFunc(logHandler)
}
func makeURL(req string) string {
return testHTTPServer.URL + req
}
func writeHeaders(w http.ResponseWriter) {
h := w.Header()
h.Add("Server", "docker-tests/mock")
h.Add("Expires", "-1")
h.Add("Content-Type", "application/json")
h.Add("Pragma", "no-cache")
h.Add("Cache-Control", "no-cache")
}
func writeResponse(w http.ResponseWriter, message any, code int) {
writeHeaders(w)
w.WriteHeader(code)
body, err := json.Marshal(message)
if err != nil {
_, _ = io.WriteString(w, err.Error())
return
}
_, _ = w.Write(body)
}
func handlerGetPing(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
writeResponse(w, true, http.StatusOK)
}
func handlerSearch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
result := &registry.SearchResults{
Query: "fakequery",
NumResults: 1,
Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
}
writeResponse(w, result, http.StatusOK)
}
func TestPing(t *testing.T) {
res, err := http.Get(makeURL("/v1/_ping"))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, res.StatusCode, http.StatusOK, "")
assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock")
_ = res.Body.Close()
}

View File

@ -0,0 +1,281 @@
package registry
import (
"testing"
"github.com/distribution/reference"
"github.com/moby/moby/api/types/registry"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestParseRepositoryInfo(t *testing.T) {
type staticRepositoryInfo struct {
Index *registry.IndexInfo
RemoteName string
CanonicalName string
LocalName string
}
tests := map[string]staticRepositoryInfo{
"fooo/bar": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "fooo/bar",
LocalName: "fooo/bar",
CanonicalName: "docker.io/fooo/bar",
},
"library/ubuntu": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "library/ubuntu",
LocalName: "ubuntu",
CanonicalName: "docker.io/library/ubuntu",
},
"nonlibrary/ubuntu": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "nonlibrary/ubuntu",
LocalName: "nonlibrary/ubuntu",
CanonicalName: "docker.io/nonlibrary/ubuntu",
},
"ubuntu": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "library/ubuntu",
LocalName: "ubuntu",
CanonicalName: "docker.io/library/ubuntu",
},
"other/library": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "other/library",
LocalName: "other/library",
CanonicalName: "docker.io/other/library",
},
"127.0.0.1:8000/private/moonbase": {
Index: &registry.IndexInfo{
Name: "127.0.0.1:8000",
Official: false,
Secure: false,
},
RemoteName: "private/moonbase",
LocalName: "127.0.0.1:8000/private/moonbase",
CanonicalName: "127.0.0.1:8000/private/moonbase",
},
"127.0.0.1:8000/privatebase": {
Index: &registry.IndexInfo{
Name: "127.0.0.1:8000",
Official: false,
Secure: false,
},
RemoteName: "privatebase",
LocalName: "127.0.0.1:8000/privatebase",
CanonicalName: "127.0.0.1:8000/privatebase",
},
"[::1]:8000/private/moonbase": {
Index: &registry.IndexInfo{
Name: "[::1]:8000",
Official: false,
Secure: false,
},
RemoteName: "private/moonbase",
LocalName: "[::1]:8000/private/moonbase",
CanonicalName: "[::1]:8000/private/moonbase",
},
"[::1]:8000/privatebase": {
Index: &registry.IndexInfo{
Name: "[::1]:8000",
Official: false,
Secure: false,
},
RemoteName: "privatebase",
LocalName: "[::1]:8000/privatebase",
CanonicalName: "[::1]:8000/privatebase",
},
// IPv6 only has a single loopback address, so ::2 is not a loopback,
// hence not marked "insecure".
"[::2]:8000/private/moonbase": {
Index: &registry.IndexInfo{
Name: "[::2]:8000",
Official: false,
Secure: true,
},
RemoteName: "private/moonbase",
LocalName: "[::2]:8000/private/moonbase",
CanonicalName: "[::2]:8000/private/moonbase",
},
// IPv6 only has a single loopback address, so ::2 is not a loopback,
// hence not marked "insecure".
"[::2]:8000/privatebase": {
Index: &registry.IndexInfo{
Name: "[::2]:8000",
Official: false,
Secure: true,
},
RemoteName: "privatebase",
LocalName: "[::2]:8000/privatebase",
CanonicalName: "[::2]:8000/privatebase",
},
"localhost:8000/private/moonbase": {
Index: &registry.IndexInfo{
Name: "localhost:8000",
Official: false,
Secure: false,
},
RemoteName: "private/moonbase",
LocalName: "localhost:8000/private/moonbase",
CanonicalName: "localhost:8000/private/moonbase",
},
"localhost:8000/privatebase": {
Index: &registry.IndexInfo{
Name: "localhost:8000",
Official: false,
Secure: false,
},
RemoteName: "privatebase",
LocalName: "localhost:8000/privatebase",
CanonicalName: "localhost:8000/privatebase",
},
"example.com/private/moonbase": {
Index: &registry.IndexInfo{
Name: "example.com",
Official: false,
Secure: true,
},
RemoteName: "private/moonbase",
LocalName: "example.com/private/moonbase",
CanonicalName: "example.com/private/moonbase",
},
"example.com/privatebase": {
Index: &registry.IndexInfo{
Name: "example.com",
Official: false,
Secure: true,
},
RemoteName: "privatebase",
LocalName: "example.com/privatebase",
CanonicalName: "example.com/privatebase",
},
"example.com:8000/private/moonbase": {
Index: &registry.IndexInfo{
Name: "example.com:8000",
Official: false,
Secure: true,
},
RemoteName: "private/moonbase",
LocalName: "example.com:8000/private/moonbase",
CanonicalName: "example.com:8000/private/moonbase",
},
"example.com:8000/privatebase": {
Index: &registry.IndexInfo{
Name: "example.com:8000",
Official: false,
Secure: true,
},
RemoteName: "privatebase",
LocalName: "example.com:8000/privatebase",
CanonicalName: "example.com:8000/privatebase",
},
"localhost/private/moonbase": {
Index: &registry.IndexInfo{
Name: "localhost",
Official: false,
Secure: false,
},
RemoteName: "private/moonbase",
LocalName: "localhost/private/moonbase",
CanonicalName: "localhost/private/moonbase",
},
"localhost/privatebase": {
Index: &registry.IndexInfo{
Name: "localhost",
Official: false,
Secure: false,
},
RemoteName: "privatebase",
LocalName: "localhost/privatebase",
CanonicalName: "localhost/privatebase",
},
IndexName + "/public/moonbase": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "public/moonbase",
LocalName: "public/moonbase",
CanonicalName: "docker.io/public/moonbase",
},
"index." + IndexName + "/public/moonbase": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "public/moonbase",
LocalName: "public/moonbase",
CanonicalName: "docker.io/public/moonbase",
},
"ubuntu-12.04-base": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "library/ubuntu-12.04-base",
LocalName: "ubuntu-12.04-base",
CanonicalName: "docker.io/library/ubuntu-12.04-base",
},
IndexName + "/ubuntu-12.04-base": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "library/ubuntu-12.04-base",
LocalName: "ubuntu-12.04-base",
CanonicalName: "docker.io/library/ubuntu-12.04-base",
},
"index." + IndexName + "/ubuntu-12.04-base": {
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
Secure: true,
},
RemoteName: "library/ubuntu-12.04-base",
LocalName: "ubuntu-12.04-base",
CanonicalName: "docker.io/library/ubuntu-12.04-base",
},
}
for reposName, expected := range tests {
t.Run(reposName, func(t *testing.T) {
named, err := reference.ParseNormalizedNamed(reposName)
assert.NilError(t, err)
repoInfo, err := ParseRepositoryInfo(named)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index))
assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName))
assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName))
assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName))
})
}
}

View File

@ -0,0 +1,86 @@
package registry
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/url"
"strings"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/moby/moby/api/types/registry"
)
// Service is a registry service. It tracks configuration data such as a list
// of mirrors.
type Service struct {
config *serviceConfig
}
// NewService returns a new instance of [Service] ready to be installed into
// an engine.
func NewService(options ServiceOptions) (*Service, error) {
config, err := newServiceConfig(options.InsecureRegistries)
if err != nil {
return nil, err
}
return &Service{config: config}, nil
}
// Auth contacts the public registry with the provided credentials,
// and returns OK if authentication was successful.
// It can be used to verify the validity of a client's credentials.
func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (token string, _ error) {
registryHostName := IndexHostname
if authConfig.ServerAddress != "" {
serverAddress := authConfig.ServerAddress
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
serverAddress = "https://" + serverAddress
}
u, err := url.Parse(serverAddress)
if err != nil {
return "", invalidParam(fmt.Errorf("unable to parse server address: %w", err))
}
registryHostName = u.Host
}
// Lookup endpoints for authentication.
endpoints, err := s.Endpoints(ctx, registryHostName)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return "", err
}
return "", invalidParam(err)
}
var lastErr error
for _, endpoint := range endpoints {
authToken, err := loginV2(ctx, authConfig, endpoint, userAgent)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) {
// Failed to authenticate; don't continue with (non-TLS) endpoints.
return "", err
}
// Try next endpoint
log.G(ctx).WithFields(log.Fields{
"error": err,
"endpoint": endpoint,
}).Infof("Error logging in to endpoint, trying next endpoint")
lastErr = err
continue
}
return authToken, nil
}
return "", lastErr
}
// APIEndpoint represents a remote API endpoint
type APIEndpoint struct {
URL *url.URL
TLSConfig *tls.Config
}

View File

@ -0,0 +1,37 @@
package registry
import (
"context"
"net/url"
"github.com/docker/go-connections/tlsconfig"
)
func (s *Service) Endpoints(ctx context.Context, hostname string) ([]APIEndpoint, error) {
if hostname == DefaultNamespace || hostname == IndexHostname {
return []APIEndpoint{{
URL: DefaultV2Registry,
TLSConfig: tlsconfig.ServerDefault(),
}}, nil
}
tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname))
if err != nil {
return nil, err
}
endpoints := []APIEndpoint{{
URL: &url.URL{Scheme: "https", Host: hostname},
TLSConfig: tlsConfig,
}}
if tlsConfig.InsecureSkipVerify {
endpoints = append(endpoints, APIEndpoint{
URL: &url.URL{Scheme: "http", Host: hostname},
// used to check if supposed to be secure via InsecureSkipVerify
TLSConfig: tlsConfig,
})
}
return endpoints, nil
}

View File

@ -15,6 +15,7 @@ replace (
require (
dario.cat/mergo v1.0.1
github.com/containerd/errdefs v1.0.0
github.com/containerd/log v0.1.0
github.com/containerd/platforms v1.0.0-rc.1
github.com/cpuguy83/go-md2man/v2 v2.0.7
github.com/creack/pty v1.1.24
@ -55,6 +56,7 @@ require (
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
github.com/xeipuuv/gojsonschema v1.2.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
@ -78,7 +80,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
@ -105,7 +106,6 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/crypto v0.37.0 // indirect

View File

@ -1,170 +0,0 @@
package registry
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/containerd/log"
"github.com/docker/distribution/registry/client/auth"
"github.com/moby/moby/api/types/filters"
"github.com/moby/moby/api/types/registry"
"github.com/pkg/errors"
)
var acceptedSearchFilterTags = map[string]bool{
"is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future.
"is-official": true,
"stars": true,
}
// Search queries the public registry for repositories matching the specified
// search term and filters.
func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
return nil, err
}
isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false)
if err != nil {
return nil, err
}
// "is-automated" is deprecated and filtering for `true` will yield no results.
if isAutomated {
return []registry.SearchResult{}, nil
}
isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false)
if err != nil {
return nil, err
}
hasStarFilter := 0
if searchFilters.Contains("stars") {
hasStars := searchFilters.Get("stars")
for _, hasStar := range hasStars {
iHasStar, err := strconv.Atoi(hasStar)
if err != nil {
return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)}
}
if iHasStar > hasStarFilter {
hasStarFilter = iHasStar
}
}
}
unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers)
if err != nil {
return nil, err
}
filteredResults := []registry.SearchResult{}
for _, result := range unfilteredResult.Results {
if searchFilters.Contains("is-official") {
if isOfficial != result.IsOfficial {
continue
}
}
if searchFilters.Contains("stars") {
if result.StarCount < hasStarFilter {
continue
}
}
// "is-automated" is deprecated and the value in Docker Hub search
// results is untrustworthy. Force it to false so as to not mislead our
// clients.
result.IsAutomated = false //nolint:staticcheck // ignore SA1019 (field is deprecated)
filteredResults = append(filteredResults, result)
}
return filteredResults, nil
}
func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
if hasScheme(term) {
return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
}
indexName, remoteName := splitReposSearchTerm(term)
// Search is a long-running operation, just lock s.config to avoid block others.
s.mu.RLock()
index := newIndexInfo(s.config, indexName)
s.mu.RUnlock()
if index.Official {
// If pull "library/foo", it's stored locally under "foo"
remoteName = strings.TrimPrefix(remoteName, "library/")
}
endpoint, err := newV1Endpoint(ctx, index, headers)
if err != nil {
return nil, err
}
var client *http.Client
if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
creds := NewStaticCredentialStore(authConfig)
// TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac)
modifiers := Headers(headers.Get("User-Agent"), nil)
v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{
auth.RegistryScope{Name: "catalog", Actions: []string{"search"}},
})
if err != nil {
return nil, err
}
// Copy non transport http client features
v2Client.Timeout = endpoint.client.Timeout
v2Client.CheckRedirect = endpoint.client.CheckRedirect
v2Client.Jar = endpoint.client.Jar
log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL)
client = v2Client
} else {
client = endpoint.client
if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil {
return nil, err
}
}
return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit)
}
// splitReposSearchTerm breaks a search term into an index name and remote name
func splitReposSearchTerm(reposName string) (string, string) {
nameParts := strings.SplitN(reposName, "/", 2)
if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
// This is a Docker Hub repository (ex: samalba/hipache or ubuntu),
// use the default Docker Hub registry (docker.io)
return IndexName, reposName
}
return nameParts[0], nameParts[1]
}
// ParseSearchIndexInfo will use repository name to get back an indexInfo.
//
// TODO(thaJeztah) this function is only used by the CLI, and used to get
// information of the registry (to provide credentials if needed). We should
// move this function (or equivalent) to the CLI, as it's doing too much just
// for that.
func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
indexName, _ := splitReposSearchTerm(reposName)
indexName = normalizeIndexName(indexName)
if indexName == IndexName {
return &registry.IndexInfo{
Name: IndexName,
Mirrors: []string{},
Secure: true,
Official: true,
}, nil
}
return &registry.IndexInfo{
Name: indexName,
Mirrors: []string{},
Secure: !isInsecure(indexName),
}, nil
}

View File

@ -1,213 +0,0 @@
package registry
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/containerd/log"
"github.com/docker/distribution/registry/client/transport"
"github.com/moby/moby/api/types/registry"
)
// v1PingResult contains the information returned when pinging a registry. It
// indicates whether the registry claims to be a standalone registry.
type v1PingResult struct {
// Standalone is set to true if the registry indicates it is a
// standalone registry in the X-Docker-Registry-Standalone
// header
Standalone bool `json:"standalone"`
}
// v1Endpoint stores basic information about a V1 registry endpoint.
type v1Endpoint struct {
client *http.Client
URL *url.URL
IsSecure bool
}
// newV1Endpoint parses the given address to return a registry endpoint.
// TODO: remove. This is only used by search.
func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure)
if err != nil {
return nil, err
}
endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers)
if err != nil {
return nil, err
}
if endpoint.String() == IndexServer {
// Skip the check, we know this one is valid
// (and we never want to fall back to http in case of error)
return endpoint, nil
}
// Try HTTPS ping to registry
endpoint.URL.Scheme = "https"
if _, err := endpoint.ping(ctx); err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
if endpoint.IsSecure {
// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP.
hint := fmt.Sprintf(
". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]s` to the daemon's arguments. "+
"In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt",
endpoint.URL.Host,
)
return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint)
}
// registry is insecure and HTTPS failed, fallback to HTTP.
log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
endpoint.URL.Scheme = "http"
if _, err2 := endpoint.ping(ctx); err2 != nil {
return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
}
}
return endpoint, nil
}
// trimV1Address trims the "v1" version suffix off the address and returns
// the trimmed address. It returns an error on "v2" endpoints.
func trimV1Address(address string) (string, error) {
trimmed := strings.TrimSuffix(address, "/")
if strings.HasSuffix(trimmed, "/v2") {
return "", invalidParamf("search is not supported on v2 endpoints: %s", address)
}
return strings.TrimSuffix(trimmed, "/v1"), nil
}
func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
address = "https://" + address
}
address, err := trimV1Address(address)
if err != nil {
return nil, err
}
uri, err := url.Parse(address)
if err != nil {
return nil, invalidParam(err)
}
// TODO(tiborvass): make sure a ConnectTimeout transport is used
tr := newTransport(tlsConfig)
return &v1Endpoint{
IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify,
URL: uri,
client: httpClient(transport.NewTransport(tr, Headers("", headers)...)),
}, nil
}
// Get the formatted URL for the root of this registry Endpoint
func (e *v1Endpoint) String() string {
return e.URL.String() + "/v1/"
}
// ping returns a v1PingResult which indicates whether the registry is standalone or not.
func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
if e.String() == IndexServer {
// Skip the check, we know this one is valid
// (and we never want to fallback to http in case of error)
return v1PingResult{}, nil
}
pingURL := e.String() + "_ping"
log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody)
if err != nil {
return v1PingResult{}, invalidParam(err)
}
resp, err := e.client.Do(req)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return v1PingResult{}, err
}
return v1PingResult{}, invalidParam(err)
}
defer resp.Body.Close()
if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" {
info := v1PingResult{}
// Accepted values are "1", and "true" (case-insensitive).
if v == "1" || strings.EqualFold(v, "true") {
info.Standalone = true
}
log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
return info, nil
}
// If the header is absent, we assume true for compatibility with earlier
// versions of the registry. default to true
info := v1PingResult{
Standalone: true,
}
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
log.G(ctx).WithError(err).Debug("error unmarshaling _ping response")
// don't stop here. Just assume sane defaults
}
log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone)
return info, nil
}
// httpClient returns an HTTP client structure which uses the given transport
// and contains the necessary headers for redirected requests
func httpClient(tr http.RoundTripper) *http.Client {
return &http.Client{
Transport: tr,
CheckRedirect: addRequiredHeadersToRedirectedRequests,
}
}
func trustedLocation(req *http.Request) bool {
var (
trusteds = []string{"docker.com", "docker.io"}
hostname = strings.SplitN(req.Host, ":", 2)[0]
)
if req.URL.Scheme != "https" {
return false
}
for _, trusted := range trusteds {
if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
return true
}
}
return false
}
// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
// for redirected requests
func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
if len(via) != 0 && via[0] != nil {
if trustedLocation(req) && trustedLocation(via[0]) {
req.Header = via[0].Header
return nil
}
for k, v := range via[0].Header {
if k != "Authorization" {
for _, vv := range v {
req.Header.Add(k, vv)
}
}
}
}
return nil
}

View File

@ -1,248 +0,0 @@
package registry
import (
// this is required for some certificates
"context"
_ "crypto/sha512"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"strings"
"sync"
"github.com/containerd/log"
"github.com/moby/moby/api/types/registry"
"github.com/pkg/errors"
)
// A session is used to communicate with a V1 registry
type session struct {
indexEndpoint *v1Endpoint
client *http.Client
}
type authTransport struct {
base http.RoundTripper
authConfig *registry.AuthConfig
alwaysSetBasicAuth bool
token []string
mu sync.Mutex // guards modReq
modReq map[*http.Request]*http.Request // original -> modified
}
// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official)
//
// For private v1 registries, set alwaysSetBasicAuth to true.
//
// For the official v1 registry, if there isn't already an Authorization header in the request,
// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
// requests.
//
// If the server sends a token without the client having requested it, it is ignored.
//
// This RoundTripper also has a CancelRequest method important for correct timeout handling.
func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport {
if base == nil {
base = http.DefaultTransport
}
return &authTransport{
base: base,
authConfig: authConfig,
alwaysSetBasicAuth: alwaysSetBasicAuth,
modReq: make(map[*http.Request]*http.Request),
}
}
// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
func cloneRequest(r *http.Request) *http.Request {
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}
return r2
}
// onEOFReader wraps an io.ReadCloser and a function
// the function will run at the end of file or close the file.
type onEOFReader struct {
Rc io.ReadCloser
Fn func()
}
func (r *onEOFReader) Read(p []byte) (int, error) {
n, err := r.Rc.Read(p)
if err == io.EOF {
r.runFunc()
}
return n, err
}
// Close closes the file and run the function.
func (r *onEOFReader) Close() error {
err := r.Rc.Close()
r.runFunc()
return err
}
func (r *onEOFReader) runFunc() {
if fn := r.Fn; fn != nil {
fn()
r.Fn = nil
}
}
// RoundTrip changes an HTTP request's headers to add the necessary
// authentication-related headers
func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
// Authorization should not be set on 302 redirect for untrusted locations.
// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
// As the authorization logic is currently implemented in RoundTrip,
// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
// This is safe as Docker doesn't set Referrer in other scenarios.
if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
return tr.base.RoundTrip(orig)
}
req := cloneRequest(orig)
tr.mu.Lock()
tr.modReq[orig] = req
tr.mu.Unlock()
if tr.alwaysSetBasicAuth {
if tr.authConfig == nil {
return nil, errors.New("unexpected error: empty auth config")
}
req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
return tr.base.RoundTrip(req)
}
// Don't override
if req.Header.Get("Authorization") == "" {
if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" {
req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
} else if len(tr.token) > 0 {
req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
}
}
resp, err := tr.base.RoundTrip(req)
if err != nil {
tr.mu.Lock()
delete(tr.modReq, orig)
tr.mu.Unlock()
return nil, err
}
if len(resp.Header["X-Docker-Token"]) > 0 {
tr.token = resp.Header["X-Docker-Token"]
}
resp.Body = &onEOFReader{
Rc: resp.Body,
Fn: func() {
tr.mu.Lock()
delete(tr.modReq, orig)
tr.mu.Unlock()
},
}
return resp, nil
}
// CancelRequest cancels an in-flight request by closing its connection.
func (tr *authTransport) CancelRequest(req *http.Request) {
type canceler interface {
CancelRequest(*http.Request)
}
if cr, ok := tr.base.(canceler); ok {
tr.mu.Lock()
modReq := tr.modReq[req]
delete(tr.modReq, req)
tr.mu.Unlock()
cr.CancelRequest(modReq)
}
}
func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error {
var alwaysSetBasicAuth bool
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
// alongside all our requests.
if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
info, err := endpoint.ping(ctx)
if err != nil {
return err
}
if info.Standalone && authConfig != nil {
log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth")
alwaysSetBasicAuth = true
}
}
// Annotate the transport unconditionally so that v2 can
// properly fallback on v1 when an image is not found.
client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth)
jar, err := cookiejar.New(nil)
if err != nil {
return systemErr{errors.New("cookiejar.New is not supposed to return an error")}
}
client.Jar = jar
return nil
}
func newSession(client *http.Client, endpoint *v1Endpoint) *session {
return &session{
client: client,
indexEndpoint: endpoint,
}
}
// defaultSearchLimit is the default value for maximum number of returned search results.
const defaultSearchLimit = 25
// searchRepositories performs a search against the remote repository
func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) {
if limit == 0 {
limit = defaultSearchLimit
}
if limit < 1 || limit > 100 {
return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
}
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit))
log.G(ctx).WithField("url", u).Debug("searchRepositories")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody)
if err != nil {
return nil, invalidParamWrapf(err, "error building request")
}
// Have the AuthTransport send authentication, when logged in.
req.Header.Set("X-Docker-Token", "true")
res, err := r.client.Do(req)
if err != nil {
return nil, systemErr{err}
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
// TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286).
// TODO(thaJeztah): handle other status-codes to return correct error-type
return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)}
}
result := &registry.SearchResults{}
err = json.NewDecoder(res.Body).Decode(result)
if err != nil {
return nil, systemErr{errors.Wrap(err, "error decoding registry search results")}
}
return result, nil
}

View File

@ -1,160 +0,0 @@
package registry
import (
"context"
"crypto/tls"
"errors"
"net/url"
"strings"
"sync"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/distribution/reference"
"github.com/moby/moby/api/types/registry"
)
// Service is a registry service. It tracks configuration data such as a list
// of mirrors.
type Service struct {
config *serviceConfig
mu sync.RWMutex
}
// NewService returns a new instance of [Service] ready to be installed into
// an engine.
func NewService(options ServiceOptions) (*Service, error) {
config, err := newServiceConfig(options)
if err != nil {
return nil, err
}
return &Service{config: config}, err
}
// ServiceConfig returns a copy of the public registry service's configuration.
func (s *Service) ServiceConfig() *registry.ServiceConfig {
s.mu.RLock()
defer s.mu.RUnlock()
return s.config.copy()
}
// ReplaceConfig prepares a transaction which will atomically replace the
// registry service's configuration when the returned commit function is called.
func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) {
config, err := newServiceConfig(options)
if err != nil {
return nil, err
}
return func() {
s.mu.Lock()
defer s.mu.Unlock()
s.config = config
}, nil
}
// Auth contacts the public registry with the provided credentials,
// and returns OK if authentication was successful.
// It can be used to verify the validity of a client's credentials.
func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) {
// TODO Use ctx when searching for repositories
registryHostName := IndexHostname
if authConfig.ServerAddress != "" {
serverAddress := authConfig.ServerAddress
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
serverAddress = "https://" + serverAddress
}
u, err := url.Parse(serverAddress)
if err != nil {
return "", "", invalidParamWrapf(err, "unable to parse server address")
}
registryHostName = u.Host
}
// Lookup endpoints for authentication but exclude mirrors to prevent
// sending credentials of the upstream registry to a mirror.
s.mu.RLock()
endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false)
s.mu.RUnlock()
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return "", "", err
}
return "", "", invalidParam(err)
}
var lastErr error
for _, endpoint := range endpoints {
authToken, err := loginV2(ctx, authConfig, endpoint, userAgent)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) {
// Failed to authenticate; don't continue with (non-TLS) endpoints.
return "", "", err
}
// Try next endpoint
log.G(ctx).WithFields(log.Fields{
"error": err,
"endpoint": endpoint,
}).Infof("Error logging in to endpoint, trying next endpoint")
lastErr = err
continue
}
// TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here?
return "Login Succeeded", authToken, nil
}
return "", "", lastErr
}
// ResolveAuthConfig looks up authentication for the given reference from the
// given authConfigs.
//
// IMPORTANT: This function is for internal use and should not be used by external projects.
func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig {
s.mu.RLock()
defer s.mu.RUnlock()
// Simplified version of "newIndexInfo" without handling of insecure
// registries and mirrors, as we don't need that information to resolve
// the auth-config.
indexName := normalizeIndexName(reference.Domain(ref))
registryInfo, ok := s.config.IndexConfigs[indexName]
if !ok {
registryInfo = &registry.IndexInfo{Name: indexName}
}
return ResolveAuthConfig(authConfigs, registryInfo)
}
// APIEndpoint represents a remote API endpoint
type APIEndpoint struct {
Mirror bool
URL *url.URL
TLSConfig *tls.Config
}
// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.lookupV2Endpoints(context.TODO(), hostname, true)
}
// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference.
// It gives preference to HTTPS over plain HTTP. Mirrors are not included.
func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.lookupV2Endpoints(context.TODO(), hostname, false)
}
// IsInsecureRegistry returns true if the registry at given host is configured as
// insecure registry.
func (s *Service) IsInsecureRegistry(host string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
return !s.config.isSecureIndex(host)
}

View File

@ -1,73 +0,0 @@
package registry
import (
"context"
"net/url"
"strings"
"github.com/docker/go-connections/tlsconfig"
)
func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includeMirrors bool) ([]APIEndpoint, error) {
var endpoints []APIEndpoint
if hostname == DefaultNamespace || hostname == IndexHostname {
if includeMirrors {
for _, mirror := range s.config.Mirrors {
if ctx.Err() != nil {
return nil, ctx.Err()
}
if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
mirror = "https://" + mirror
}
mirrorURL, err := url.Parse(mirror)
if err != nil {
return nil, invalidParam(err)
}
// TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time.
mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host))
if err != nil {
return nil, err
}
endpoints = append(endpoints, APIEndpoint{
URL: mirrorURL,
Mirror: true,
TLSConfig: mirrorTLSConfig,
})
}
}
endpoints = append(endpoints, APIEndpoint{
URL: DefaultV2Registry,
TLSConfig: tlsconfig.ServerDefault(),
})
return endpoints, nil
}
tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname))
if err != nil {
return nil, err
}
endpoints = []APIEndpoint{
{
URL: &url.URL{
Scheme: "https",
Host: hostname,
},
TLSConfig: tlsConfig,
},
}
if tlsConfig.InsecureSkipVerify {
endpoints = append(endpoints, APIEndpoint{
URL: &url.URL{
Scheme: "http",
Host: hostname,
},
// used to check if supposed to be secure via InsecureSkipVerify
TLSConfig: tlsConfig,
})
}
return endpoints, nil
}

1
vendor/modules.txt vendored
View File

@ -71,7 +71,6 @@ github.com/docker/docker/pkg/jsonmessage
github.com/docker/docker/pkg/process
github.com/docker/docker/pkg/progress
github.com/docker/docker/pkg/streamformatter
github.com/docker/docker/registry
# github.com/docker/docker-credential-helpers v0.9.3
## explicit; go 1.21
github.com/docker/docker-credential-helpers/client