mirror of https://github.com/docker/cli.git
Merge pull request #6221 from thaJeztah/28.x_fork_registry
[28.x] add internal fork of docker/docker/registry
This commit is contained in:
commit
0718529a7e
|
|
@ -16,11 +16,11 @@ 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/api/types/auxprogress"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/morikuni/aec"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -105,10 +105,10 @@ To push the complete multi-platform image, remove the --platform flag.
|
|||
}
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
indexInfo := registry.NewIndexInfo(ref)
|
||||
|
||||
// Resolve the Auth config relevant for this server
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index)
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo)
|
||||
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -134,7 +134,7 @@ To push the complete multi-platform image, remove the --platform flag.
|
|||
defer responseBody.Close()
|
||||
if !opts.untrusted {
|
||||
// TODO pushTrustedReference currently doesn't respect `--quiet`
|
||||
return pushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody)
|
||||
return pushTrustedReference(ctx, dockerCli, indexInfo, ref, authConfig, responseBody)
|
||||
}
|
||||
|
||||
if opts.quiet {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
@ -42,7 +41,11 @@ func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth) (
|
|||
}
|
||||
|
||||
// pushTrustedReference pushes a canonical reference to the trust server.
|
||||
func pushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error {
|
||||
func pushTrustedReference(ctx context.Context, ioStreams command.Streams, indexInfo *registrytypes.IndexInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error {
|
||||
repoInfo := &trust.RepositoryInfo{
|
||||
Name: reference.TrimNamed(ref),
|
||||
Index: indexInfo,
|
||||
}
|
||||
return trust.PushTrustedReference(ctx, ioStreams, repoInfo, ref, authConfig, in, command.UserAgent())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import (
|
|||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
|
@ -65,8 +65,7 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti
|
|||
return types.PluginInstallOptions{}, err
|
||||
}
|
||||
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
|
||||
indexInfo := registry.NewIndexInfo(ref)
|
||||
remote := ref.String()
|
||||
|
||||
_, isCanonical := ref.(reference.Canonical)
|
||||
|
|
@ -84,7 +83,7 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti
|
|||
remote = reference.FamiliarString(trusted)
|
||||
}
|
||||
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index)
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo)
|
||||
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
||||
if err != nil {
|
||||
return types.PluginInstallOptions{}, err
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -49,8 +49,8 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
|||
|
||||
named = reference.TagNameOnly(named)
|
||||
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(named)
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index)
|
||||
indexInfo := registry.NewIndexInfo(named)
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo)
|
||||
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -63,6 +63,10 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
|||
defer responseBody.Close()
|
||||
|
||||
if !opts.untrusted {
|
||||
repoInfo := &trust.RepositoryInfo{
|
||||
Name: reference.TrimNamed(named),
|
||||
Index: indexInfo,
|
||||
}
|
||||
return trust.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody, command.UserAgent())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ 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"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ 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"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ package registry
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/opts"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -52,13 +52,7 @@ func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions
|
|||
if options.filter.Value().Contains("is-automated") {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), `WARNING: the "is-automated" filter is deprecated, and searching for "is-automated=true" will not yield any results in future.`)
|
||||
}
|
||||
indexInfo, err := registry.ParseSearchIndexInfo(options.term)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo)
|
||||
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
||||
encodedAuth, err := getAuth(dockerCli, options.term)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -80,3 +74,37 @@ func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions
|
|||
}
|
||||
return SearchWrite(searchCtx, results)
|
||||
}
|
||||
|
||||
// authConfigKey is the key used to store credentials for Docker Hub. It is
|
||||
// a copy of [registry.IndexServer].
|
||||
//
|
||||
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer
|
||||
const authConfigKey = "https://index.docker.io/v1/"
|
||||
|
||||
// getAuth will use fetch auth based on the given search-term. If the search
|
||||
// does not contain a hostname for the registry, it assumes Docker Hub is used,
|
||||
// and resolves authentication for Docker Hub, otherwise it resolves authentication
|
||||
// for the given registry.
|
||||
func getAuth(dockerCLI command.Cli, reposName string) (encodedAuth string, err error) {
|
||||
authCfgKey := splitReposSearchTerm(reposName)
|
||||
if authCfgKey == "docker.io" || authCfgKey == "index.docker.io" {
|
||||
authCfgKey = authConfigKey
|
||||
}
|
||||
|
||||
// Ignoring errors here, which was the existing behavior (likely
|
||||
// "no credentials found"). We'll get an error when search failed,
|
||||
// so fine to ignore in most situations.
|
||||
authConfig, _ := dockerCLI.ConfigFile().GetAuthConfig(authCfgKey)
|
||||
return registrytypes.EncodeAuthConfig(registrytypes.AuthConfig(authConfig))
|
||||
}
|
||||
|
||||
// splitReposSearchTerm breaks a search term into an index name and remote name
|
||||
func splitReposSearchTerm(reposName 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 "docker.io"
|
||||
}
|
||||
return nameParts[0]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
@ -51,9 +51,12 @@ func resolveServiceImageDigestContentTrust(dockerCli command.Cli, service *swarm
|
|||
}
|
||||
|
||||
func trustedResolveDigest(cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) {
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index)
|
||||
|
||||
indexInfo := registry.NewIndexInfo(ref)
|
||||
authConfig := command.ResolveAuthConfig(cli.ConfigFile(), indexInfo)
|
||||
repoInfo := &trust.RepositoryInfo{
|
||||
Name: reference.TrimNamed(ref),
|
||||
Index: indexInfo,
|
||||
}
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error establishing connection to trust repository")
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ 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/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ var sampleInfoNoSwarm = system.Info{
|
|||
IndexConfigs: map[string]*registrytypes.IndexInfo{
|
||||
"docker.io": {
|
||||
Name: "docker.io",
|
||||
Mirrors: nil,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
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"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
@ -32,8 +34,7 @@ func (r repositoryEndpoint) BaseURL() string {
|
|||
|
||||
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
|
||||
repoName := reference.TrimNamed(ref)
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
indexInfo := repoInfo.Index
|
||||
indexInfo := registry.NewIndexInfo(ref)
|
||||
|
||||
endpoint, err := getDefaultEndpoint(ref, !indexInfo.Secure)
|
||||
if err != nil {
|
||||
|
|
@ -54,7 +55,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 +98,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 +118,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) {}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -221,8 +221,7 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
|
|||
}
|
||||
|
||||
repoName := reference.TrimNamed(namedRef)
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
|
||||
indexInfo := repoInfo.Index
|
||||
indexInfo := registry.NewIndexInfo(namedRef)
|
||||
|
||||
confirmedTLSRegistries := make(map[string]bool)
|
||||
for _, endpoint := range endpoints {
|
||||
|
|
@ -283,10 +282,9 @@ 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(namedRef))
|
||||
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
|
||||
return endpoints, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ 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"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -95,7 +95,7 @@ func (simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {}
|
|||
// GetNotaryRepository returns a NotaryRepository which stores all the
|
||||
// information needed to operate on a notary repository.
|
||||
// It creates an HTTP transport providing authentication support.
|
||||
func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) {
|
||||
func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) {
|
||||
server, err := Server(repoInfo.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -304,11 +304,18 @@ type ImageRefAndAuth struct {
|
|||
original string
|
||||
authConfig *registrytypes.AuthConfig
|
||||
reference reference.Named
|
||||
repoInfo *registry.RepositoryInfo
|
||||
repoInfo *RepositoryInfo
|
||||
tag string
|
||||
digest digest.Digest
|
||||
}
|
||||
|
||||
// RepositoryInfo describes a repository
|
||||
type RepositoryInfo struct {
|
||||
Name reference.Named
|
||||
// Index points to registry information
|
||||
Index *registrytypes.IndexInfo
|
||||
}
|
||||
|
||||
// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name
|
||||
// as an ImageRefAndAuth struct
|
||||
func GetImageReferencesAndAuth(ctx context.Context,
|
||||
|
|
@ -321,15 +328,18 @@ func GetImageReferencesAndAuth(ctx context.Context,
|
|||
}
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
authConfig := authResolver(ctx, repoInfo.Index)
|
||||
indexInfo := registry.NewIndexInfo(ref)
|
||||
authConfig := authResolver(ctx, indexInfo)
|
||||
return ImageRefAndAuth{
|
||||
original: imgName,
|
||||
authConfig: &authConfig,
|
||||
reference: ref,
|
||||
repoInfo: repoInfo,
|
||||
tag: getTag(ref),
|
||||
digest: getDigest(ref),
|
||||
repoInfo: &RepositoryInfo{
|
||||
Name: reference.TrimNamed(ref),
|
||||
Index: indexInfo,
|
||||
},
|
||||
tag: getTag(ref),
|
||||
digest: getDigest(ref),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -366,7 +376,7 @@ func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named {
|
|||
}
|
||||
|
||||
// RepoInfo returns the repository information for a given ImageRefAndAuth
|
||||
func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo {
|
||||
func (imgRefAuth *ImageRefAndAuth) RepoInfo() *RepositoryInfo {
|
||||
return imgRefAuth.repoInfo
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
|
|
@ -32,7 +31,7 @@ type Streams interface {
|
|||
// PushTrustedReference pushes a canonical reference to the trust server.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error {
|
||||
func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error {
|
||||
// If it is a trusted push we would like to find the target entry which match the
|
||||
// tag provided in the function and then do an AddTarget later.
|
||||
notaryTarget := &client.Target{}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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/docker/docker/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(auth *registry.AuthConfig) auth.CredentialStore {
|
||||
return staticCredentialStore{
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
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 (scs 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,64 +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(url string) string {
|
||||
stripped := url
|
||||
if strings.HasPrefix(stripped, "http://") {
|
||||
stripped = strings.TrimPrefix(stripped, "http://")
|
||||
} else if strings.HasPrefix(stripped, "https://") {
|
||||
stripped = strings.TrimPrefix(stripped, "https://")
|
||||
}
|
||||
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, transport http.RoundTripper) (challenge.Manager, error) {
|
||||
pingClient := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
|
||||
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
|
||||
|
|
@ -193,9 +119,7 @@ func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.M
|
|||
|
||||
challengeManager := challenge.NewSimpleManager()
|
||||
if err := challengeManager.AddResponse(resp); err != nil {
|
||||
return nil, PingResponseError{
|
||||
Err: err,
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return challengeManager, nil
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
)
|
||||
|
||||
// ServiceOptions holds command line options.
|
||||
//
|
||||
// TODO(thaJeztah): add CertsDir as option to replace the [CertsDir] function, which sets the location magically.
|
||||
type ServiceOptions struct {
|
||||
InsecureRegistries []string `json:"insecure-registries,omitempty"`
|
||||
}
|
||||
|
||||
// serviceConfig holds daemon configuration for the registry service.
|
||||
//
|
||||
// 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
|
||||
// are still supported by Docker Hub (and will continue to be supported), but
|
||||
// there are new domains already in use, and plans to consolidate all legacy
|
||||
// domains to new "canonical" domains. Once those domains are decided on, we
|
||||
// should update these consts (but making sure to preserve compatibility with
|
||||
// existing installs, clients, and user configuration).
|
||||
const (
|
||||
// DefaultNamespace is the default namespace
|
||||
DefaultNamespace = "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://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
|
||||
// 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() + `$`)
|
||||
})
|
||||
)
|
||||
|
||||
// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit],
|
||||
// but inlining it to prevent adding that as a dependency for docker/cli.
|
||||
//
|
||||
// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8
|
||||
func runningWithRootlessKit() bool {
|
||||
return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != ""
|
||||
}
|
||||
|
||||
// CertsDir is the directory where certificates are stored.
|
||||
//
|
||||
// - Linux: "/etc/docker/certs.d/"
|
||||
// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/"
|
||||
// - Windows: "%PROGRAMDATA%/docker/certs.d/"
|
||||
//
|
||||
// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless.
|
||||
func CertsDir() string {
|
||||
certsDir := "/etc/docker/certs.d"
|
||||
if runningWithRootlessKit() {
|
||||
if configHome, _ := os.UserConfigDir(); configHome != "" {
|
||||
certsDir = filepath.Join(configHome, "docker", "certs.d")
|
||||
}
|
||||
} else if runtime.GOOS == "windows" {
|
||||
certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d")
|
||||
}
|
||||
return certsDir
|
||||
}
|
||||
|
||||
// newServiceConfig creates a new service config with the given options.
|
||||
func newServiceConfig(registries []string) (*serviceConfig, error) {
|
||||
if len(registries) == 0 {
|
||||
return &serviceConfig{}, nil
|
||||
}
|
||||
// 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([]*net.IPNet, 0)
|
||||
indexConfigs = make(map[string]*registry.IndexInfo)
|
||||
)
|
||||
|
||||
skip:
|
||||
for _, r := range registries {
|
||||
if scheme, host, ok := strings.Cut(r, "://"); ok {
|
||||
switch strings.ToLower(scheme) {
|
||||
case "http", "https":
|
||||
log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme)
|
||||
r = host
|
||||
default:
|
||||
// unsupported scheme
|
||||
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.
|
||||
for _, value := range insecureRegistryCIDRs {
|
||||
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, ipnet)
|
||||
} else {
|
||||
if err := validateHostPort(r); err != nil {
|
||||
return nil, invalidParam(fmt.Errorf("insecure registry %s is not valid: %w", r, err))
|
||||
}
|
||||
// Assume `host:port` if not CIDR.
|
||||
indexConfigs[r] = ®istry.IndexInfo{
|
||||
Name: r,
|
||||
Secure: false,
|
||||
Official: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure public registry.
|
||||
indexConfigs[IndexName] = ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
|
||||
return &serviceConfig{
|
||||
indexConfigs: indexConfigs,
|
||||
insecureRegistryCIDRs: insecureRegistryCIDRs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isSecureIndex returns false if the provided indexName is part of the list of insecure registries
|
||||
// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
|
||||
//
|
||||
// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet.
|
||||
// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered
|
||||
// insecure.
|
||||
//
|
||||
// indexName should be 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 in order to check if the IP is contained
|
||||
// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element
|
||||
// of insecureRegistries.
|
||||
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 {
|
||||
return index.Secure
|
||||
}
|
||||
|
||||
return !isCIDRMatch(config.insecureRegistryCIDRs, indexName)
|
||||
}
|
||||
|
||||
// for mocking in unit tests.
|
||||
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 []*net.IPNet, urlHost string) bool {
|
||||
if len(cidrs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(urlHost)
|
||||
if err != nil {
|
||||
// Assume urlHost is a host without port and go on.
|
||||
host = urlHost
|
||||
}
|
||||
|
||||
var addresses []net.IP
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
// Host is an IP-address.
|
||||
addresses = append(addresses, ip)
|
||||
} else {
|
||||
// Try to resolve the host's IP-address.
|
||||
addresses, err = lookupIP(host)
|
||||
if err != nil {
|
||||
// We failed to resolve the host; assume there's no match.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
for _, ipnet := range cidrs {
|
||||
// check if the addr falls in the subnet
|
||||
if ipnet.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeIndexName(val string) string {
|
||||
if val == "index.docker.io" {
|
||||
return "docker.io"
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
host = s
|
||||
port = ""
|
||||
}
|
||||
// If match against the `host:port` pattern fails,
|
||||
// it might be `IPv6:port`, which will be captured by net.ParseIP(host)
|
||||
if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil {
|
||||
return invalidParamf("invalid host %q", host)
|
||||
}
|
||||
if port != "" {
|
||||
v, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v < 0 || v > 65535 {
|
||||
return invalidParamf("invalid port %q", port)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewIndexInfo creates a new [registry.IndexInfo] or the given
|
||||
// repository-name, and detects whether the registry is considered
|
||||
// "secure" (non-localhost).
|
||||
func NewIndexInfo(reposName reference.Named) *registry.IndexInfo {
|
||||
indexName := normalizeIndexName(reference.Domain(reposName))
|
||||
if indexName == IndexName {
|
||||
return ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
}
|
||||
|
||||
return ®istry.IndexInfo{
|
||||
Name: indexName,
|
||||
Secure: !isInsecure(indexName),
|
||||
}
|
||||
}
|
||||
|
||||
// isInsecure is used to detect whether a registry domain or IP-address is allowed
|
||||
// to use an insecure (non-TLS, or self-signed cert) connection according to the
|
||||
// defaults, which allows for insecure connections with registries running on a
|
||||
// loopback address ("localhost", "::1/128", "127.0.0.0/8").
|
||||
//
|
||||
// It is used in situations where we don't have access to the daemon's configuration,
|
||||
// for example, when used from the client / CLI.
|
||||
func isInsecure(hostNameOrIP string) bool {
|
||||
// Attempt to strip port if present; this also strips brackets for
|
||||
// IPv6 addresses with a port (e.g. "[::1]:5000").
|
||||
//
|
||||
// This is best-effort; we'll continue using the address as-is if it fails.
|
||||
if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil {
|
||||
hostNameOrIP = host
|
||||
}
|
||||
if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") {
|
||||
// Fast path; no need to resolve these, assuming nobody overrides
|
||||
// "localhost" for anything else than a loopback address (sorry, not sorry).
|
||||
return true
|
||||
}
|
||||
|
||||
var addresses []net.IP
|
||||
if ip := net.ParseIP(hostNameOrIP); ip != nil {
|
||||
addresses = append(addresses, ip)
|
||||
} else {
|
||||
// Try to resolve the host's IP-addresses.
|
||||
addrs, _ := lookupIP(hostNameOrIP)
|
||||
addresses = append(addresses, addrs...)
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
if addr.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
func translateV2AuthError(err error) error {
|
||||
var e *url.Error
|
||||
if errors.As(err, &e) {
|
||||
var e2 errcode.Error
|
||||
if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) {
|
||||
return unauthorizedErr{err}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func invalidParam(err error) error {
|
||||
return invalidParameterErr{err}
|
||||
}
|
||||
|
||||
func invalidParamf(format string, args ...any) error {
|
||||
return invalidParameterErr{fmt.Errorf(format, args...)}
|
||||
}
|
||||
|
||||
type unauthorizedErr struct{ error }
|
||||
|
||||
func (unauthorizedErr) Unauthorized() {}
|
||||
|
||||
func (e unauthorizedErr) Cause() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func (e unauthorizedErr) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
type invalidParameterErr struct{ error }
|
||||
|
||||
func (invalidParameterErr) InvalidParameter() {}
|
||||
|
||||
func (e invalidParameterErr) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
|
@ -4,10 +4,13 @@ package registry
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/log"
|
||||
|
|
@ -16,16 +19,15 @@ import (
|
|||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
// HostCertsDir returns the config directory for a specific host.
|
||||
//
|
||||
// Deprecated: this function was only used internally, and will be removed in a future release.
|
||||
func HostCertsDir(hostname string) string {
|
||||
return hostCertsDir(hostname)
|
||||
}
|
||||
|
||||
// hostCertsDir returns the config directory for a specific host.
|
||||
func hostCertsDir(hostname string) string {
|
||||
return filepath.Join(CertsDir(), cleanPath(hostname))
|
||||
func hostCertsDir(hostnameAndPort string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Ensure that a directory name is valid; hostnameAndPort may contain
|
||||
// a colon (:) if a port is included, and Windows does not allow colons
|
||||
// in directory names.
|
||||
hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", ""))
|
||||
}
|
||||
return filepath.Join(CertsDir(), hostnameAndPort)
|
||||
}
|
||||
|
||||
// newTLSConfig constructs a client TLS configuration based on server defaults
|
||||
|
|
@ -81,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
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/docker/docker/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 := ®istry.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()
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestNewIndexInfo(t *testing.T) {
|
||||
type staticRepositoryInfo struct {
|
||||
Index *registry.IndexInfo
|
||||
RemoteName string
|
||||
CanonicalName string
|
||||
LocalName string
|
||||
}
|
||||
|
||||
tests := map[string]staticRepositoryInfo{
|
||||
"fooo/bar": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "fooo/bar",
|
||||
LocalName: "fooo/bar",
|
||||
CanonicalName: "docker.io/fooo/bar",
|
||||
},
|
||||
"library/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"nonlibrary/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "nonlibrary/ubuntu",
|
||||
LocalName: "nonlibrary/ubuntu",
|
||||
CanonicalName: "docker.io/nonlibrary/ubuntu",
|
||||
},
|
||||
"ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"other/library": {
|
||||
Index: ®istry.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: ®istry.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: ®istry.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: ®istry.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: ®istry.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: ®istry.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: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::2]:8000/privatebase",
|
||||
CanonicalName: "[::2]:8000/privatebase",
|
||||
},
|
||||
"localhost:8000/private/moonbase": {
|
||||
Index: ®istry.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: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost:8000/privatebase",
|
||||
CanonicalName: "localhost:8000/privatebase",
|
||||
},
|
||||
"example.com/private/moonbase": {
|
||||
Index: ®istry.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: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com/privatebase",
|
||||
CanonicalName: "example.com/privatebase",
|
||||
},
|
||||
"example.com:8000/private/moonbase": {
|
||||
Index: ®istry.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: ®istry.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: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost/private/moonbase",
|
||||
CanonicalName: "localhost/private/moonbase",
|
||||
},
|
||||
"localhost/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost/privatebase",
|
||||
CanonicalName: "localhost/privatebase",
|
||||
},
|
||||
IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"index." + IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"ubuntu-12.04-base": {
|
||||
Index: ®istry.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: ®istry.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: ®istry.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)
|
||||
|
||||
indexInfo := NewIndexInfo(named)
|
||||
repoInfoName := reference.TrimNamed(named)
|
||||
|
||||
assert.Check(t, is.DeepEqual(indexInfo, expected.Index))
|
||||
assert.Check(t, is.Equal(reference.Path(repoInfoName), expected.RemoteName))
|
||||
assert.Check(t, is.Equal(reference.FamiliarName(repoInfoName), expected.LocalName))
|
||||
assert.Check(t, is.Equal(repoInfoName.Name(), expected.CanonicalName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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/docker/docker/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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ go 1.23.0
|
|||
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
|
||||
|
|
@ -47,6 +48,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
|
||||
|
|
@ -70,7 +72,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
|
||||
|
|
@ -97,7 +98,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/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
package homedir
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Get returns the home directory of the current user with the help of
|
||||
// environment variables depending on the target operating system.
|
||||
// Returned path should be used with "path/filepath" to form new paths.
|
||||
//
|
||||
// On non-Windows platforms, it falls back to nss lookups, if the home
|
||||
// directory cannot be obtained from environment-variables.
|
||||
//
|
||||
// If linking statically with cgo enabled against glibc, ensure the
|
||||
// osusergo build tag is used.
|
||||
//
|
||||
// If needing to do nss lookups, do not disable cgo or set osusergo.
|
||||
func Get() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
if home == "" && runtime.GOOS != "windows" {
|
||||
if u, err := user.Current(); err == nil {
|
||||
return u.HomeDir
|
||||
}
|
||||
}
|
||||
return home
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package homedir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetRuntimeDir returns XDG_RUNTIME_DIR.
|
||||
// XDG_RUNTIME_DIR is typically configured via pam_systemd.
|
||||
// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetRuntimeDir() (string, error) {
|
||||
if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
|
||||
return xdgRuntimeDir, nil
|
||||
}
|
||||
return "", errors.New("could not get XDG_RUNTIME_DIR")
|
||||
}
|
||||
|
||||
// StickRuntimeDirContents sets the sticky bit on files that are under
|
||||
// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system.
|
||||
//
|
||||
// StickyRuntimeDir returns slice of sticked files.
|
||||
// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func StickRuntimeDirContents(files []string) ([]string, error) {
|
||||
runtimeDir, err := GetRuntimeDir()
|
||||
if err != nil {
|
||||
// ignore error if runtimeDir is empty
|
||||
return nil, nil
|
||||
}
|
||||
runtimeDir, err = filepath.Abs(runtimeDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sticked []string
|
||||
for _, f := range files {
|
||||
f, err = filepath.Abs(f)
|
||||
if err != nil {
|
||||
return sticked, err
|
||||
}
|
||||
if strings.HasPrefix(f, runtimeDir+"/") {
|
||||
if err = stick(f); err != nil {
|
||||
return sticked, err
|
||||
}
|
||||
sticked = append(sticked, f)
|
||||
}
|
||||
}
|
||||
return sticked, nil
|
||||
}
|
||||
|
||||
func stick(f string) error {
|
||||
st, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := st.Mode()
|
||||
m |= os.ModeSticky
|
||||
return os.Chmod(f, m)
|
||||
}
|
||||
|
||||
// GetDataHome returns XDG_DATA_HOME.
|
||||
// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set.
|
||||
// If HOME and XDG_DATA_HOME are not set, getpwent(3) is consulted to determine the users home directory.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetDataHome() (string, error) {
|
||||
if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
|
||||
return xdgDataHome, nil
|
||||
}
|
||||
home := Get()
|
||||
if home == "" {
|
||||
return "", errors.New("could not get either XDG_DATA_HOME or HOME")
|
||||
}
|
||||
return filepath.Join(home, ".local", "share"), nil
|
||||
}
|
||||
|
||||
// GetConfigHome returns XDG_CONFIG_HOME.
|
||||
// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set.
|
||||
// If HOME and XDG_CONFIG_HOME are not set, getpwent(3) is consulted to determine the users home directory.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetConfigHome() (string, error) {
|
||||
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
|
||||
return xdgConfigHome, nil
|
||||
}
|
||||
home := Get()
|
||||
if home == "" {
|
||||
return "", errors.New("could not get either XDG_CONFIG_HOME or HOME")
|
||||
}
|
||||
return filepath.Join(home, ".config"), nil
|
||||
}
|
||||
|
||||
// GetLibHome returns $HOME/.local/lib
|
||||
// If HOME is not set, getpwent(3) is consulted to determine the users home directory.
|
||||
func GetLibHome() (string, error) {
|
||||
home := Get()
|
||||
if home == "" {
|
||||
return "", errors.New("could not get HOME")
|
||||
}
|
||||
return filepath.Join(home, ".local/lib"), nil
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
//go:build !linux
|
||||
|
||||
package homedir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// GetRuntimeDir is unsupported on non-linux system.
|
||||
func GetRuntimeDir() (string, error) {
|
||||
return "", errors.New("homedir.GetRuntimeDir() is not supported on this system")
|
||||
}
|
||||
|
||||
// StickRuntimeDirContents is unsupported on non-linux system.
|
||||
func StickRuntimeDirContents(files []string) ([]string, error) {
|
||||
return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetDataHome is unsupported on non-linux system.
|
||||
func GetDataHome() (string, error) {
|
||||
return "", errors.New("homedir.GetDataHome() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetConfigHome is unsupported on non-linux system.
|
||||
func GetConfigHome() (string, error) {
|
||||
return "", errors.New("homedir.GetConfigHome() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetLibHome is unsupported on non-linux system.
|
||||
func GetLibHome() (string, error) {
|
||||
return "", errors.New("homedir.GetLibHome() is not supported on this system")
|
||||
}
|
||||
|
|
@ -1,481 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/internal/lazyregexp"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
)
|
||||
|
||||
// ServiceOptions holds command line options.
|
||||
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
|
||||
|
||||
// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains
|
||||
// are here for historic reasons and backward-compatibility. These domains
|
||||
// are still supported by Docker Hub (and will continue to be supported), but
|
||||
// there are new domains already in use, and plans to consolidate all legacy
|
||||
// domains to new "canonical" domains. Once those domains are decided on, we
|
||||
// should update these consts (but making sure to preserve compatibility with
|
||||
// existing installs, clients, and user configuration).
|
||||
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/"
|
||||
// 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,
|
||||
}
|
||||
|
||||
validHostPortRegex = lazyregexp.New(`^` + reference.DomainRegexp.String() + `$`)
|
||||
|
||||
// certsDir is used to override defaultCertsDir when running with rootlessKit.
|
||||
//
|
||||
// TODO(thaJeztah): change to a sync.OnceValue once we remove [SetCertsDir]
|
||||
// TODO(thaJeztah): certsDir should not be a package variable, but stored in our config, and passed when needed.
|
||||
setCertsDirOnce sync.Once
|
||||
certsDir string
|
||||
)
|
||||
|
||||
func setCertsDir(dir string) string {
|
||||
setCertsDirOnce.Do(func() {
|
||||
if dir != "" {
|
||||
certsDir = dir
|
||||
return
|
||||
}
|
||||
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
|
||||
// Configure registry.CertsDir() when running in rootless-mode
|
||||
// This is the equivalent of [rootless.RunningWithRootlessKit],
|
||||
// but inlining it to prevent adding that as a dependency
|
||||
// for docker/cli.
|
||||
//
|
||||
// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8
|
||||
if configHome, _ := homedir.GetConfigHome(); configHome != "" {
|
||||
certsDir = filepath.Join(configHome, "docker/certs.d")
|
||||
return
|
||||
}
|
||||
}
|
||||
certsDir = defaultCertsDir
|
||||
})
|
||||
return certsDir
|
||||
}
|
||||
|
||||
// SetCertsDir allows the default certs directory to be changed. This function
|
||||
// is used at daemon startup to set the correct location when running in
|
||||
// rootless mode.
|
||||
//
|
||||
// Deprecated: the cert-directory is now automatically selected when running with rootlessKit, and should no longer be set manually.
|
||||
func SetCertsDir(path string) {
|
||||
setCertsDir(path)
|
||||
}
|
||||
|
||||
// CertsDir is the directory where certificates are stored.
|
||||
func CertsDir() string {
|
||||
// call setCertsDir with an empty path to synchronise with [SetCertsDir]
|
||||
return setCertsDir("")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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 ®istry.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)
|
||||
indexConfigs = make(map[string]*registry.IndexInfo)
|
||||
)
|
||||
|
||||
skip:
|
||||
for _, r := range registries {
|
||||
// validate insecure registry
|
||||
if _, err := ValidateIndexName(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(r), "http://") {
|
||||
log.G(context.TODO()).Warnf("insecure registry %s should not contain 'http://' and 'http://' has been removed from the insecure registry config", r)
|
||||
r = r[7:]
|
||||
} else if strings.HasPrefix(strings.ToLower(r), "https://") {
|
||||
log.G(context.TODO()).Warnf("insecure registry %s should not contain 'https://' and 'https://' has been removed from the insecure registry config", r)
|
||||
r = r[8:]
|
||||
} else if hasScheme(r) {
|
||||
return invalidParamf("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() {
|
||||
continue skip
|
||||
}
|
||||
}
|
||||
// ipnet is not found, add it in config.InsecureRegistryCIDRs
|
||||
insecureRegistryCIDRs = append(insecureRegistryCIDRs, data)
|
||||
} else {
|
||||
if err := validateHostPort(r); err != nil {
|
||||
return invalidParamWrapf(err, "insecure registry %s is not valid", r)
|
||||
}
|
||||
// Assume `host:port` if not CIDR.
|
||||
indexConfigs[r] = ®istry.IndexInfo{
|
||||
Name: r,
|
||||
Mirrors: []string{},
|
||||
Secure: false,
|
||||
Official: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure public registry.
|
||||
indexConfigs[IndexName] = ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: config.Mirrors,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
config.InsecureRegistryCIDRs = insecureRegistryCIDRs
|
||||
config.IndexConfigs = indexConfigs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSecureIndex returns false if the provided indexName is part of the list of insecure registries
|
||||
// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
|
||||
//
|
||||
// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet.
|
||||
// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered
|
||||
// insecure.
|
||||
//
|
||||
// indexName should be 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 in order to check if the IP is contained
|
||||
// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element
|
||||
// of insecureRegistries.
|
||||
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 {
|
||||
return index.Secure
|
||||
}
|
||||
|
||||
return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName)
|
||||
}
|
||||
|
||||
// for mocking in unit tests.
|
||||
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 {
|
||||
if len(cidrs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(URLHost)
|
||||
if err != nil {
|
||||
// Assume URLHost is a host without port and go on.
|
||||
host = URLHost
|
||||
}
|
||||
|
||||
var addresses []net.IP
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
// Host is an IP-address.
|
||||
addresses = append(addresses, ip)
|
||||
} else {
|
||||
// Try to resolve the host's IP-address.
|
||||
addresses, err = lookupIP(host)
|
||||
if err != nil {
|
||||
// We failed to resolve the host; assume there's no match.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
for _, ipnet := range cidrs {
|
||||
// check if the addr falls in the subnet
|
||||
if (*net.IPNet)(ipnet).Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
host = s
|
||||
port = ""
|
||||
}
|
||||
// If match against the `host:port` pattern fails,
|
||||
// it might be `IPv6:port`, which will be captured by net.ParseIP(host)
|
||||
if !validHostPortRegex.MatchString(s) && net.ParseIP(host) == nil {
|
||||
return invalidParamf("invalid host %q", host)
|
||||
}
|
||||
if port != "" {
|
||||
v, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v < 0 || v > 65535 {
|
||||
return invalidParamf("invalid port %q", port)
|
||||
}
|
||||
}
|
||||
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 ®istry.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
|
||||
}
|
||||
|
||||
// newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo
|
||||
func newRepositoryInfo(config *serviceConfig, name reference.Named) *RepositoryInfo {
|
||||
index := newIndexInfo(config, reference.Domain(name))
|
||||
var officialRepo bool
|
||||
if index.Official {
|
||||
// RepositoryInfo.Official indicates whether the image repository
|
||||
// is an official (docker library official images) repository.
|
||||
//
|
||||
// We only need to check this if the image-repository is on Docker Hub.
|
||||
officialRepo = !strings.ContainsRune(reference.FamiliarName(name), '/')
|
||||
}
|
||||
|
||||
return &RepositoryInfo{
|
||||
Name: reference.TrimNamed(name),
|
||||
Index: index,
|
||||
Official: officialRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRepositoryInfo performs the breakdown of a repository name into a
|
||||
// [RepositoryInfo], but lacks registry configuration.
|
||||
//
|
||||
// It is used by the Docker cli to interact with registry-related endpoints.
|
||||
func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
|
||||
indexName := normalizeIndexName(reference.Domain(reposName))
|
||||
if indexName == IndexName {
|
||||
return &RepositoryInfo{
|
||||
Name: reference.TrimNamed(reposName),
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Secure: true,
|
||||
Official: true,
|
||||
},
|
||||
Official: !strings.ContainsRune(reference.FamiliarName(reposName), '/'),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &RepositoryInfo{
|
||||
Name: reference.TrimNamed(reposName),
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: indexName,
|
||||
Mirrors: []string{},
|
||||
Secure: !isInsecure(indexName),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isInsecure is used to detect whether a registry domain or IP-address is allowed
|
||||
// to use an insecure (non-TLS, or self-signed cert) connection according to the
|
||||
// defaults, which allows for insecure connections with registries running on a
|
||||
// loopback address ("localhost", "::1/128", "127.0.0.0/8").
|
||||
//
|
||||
// It is used in situations where we don't have access to the daemon's configuration,
|
||||
// for example, when used from the client / CLI.
|
||||
func isInsecure(hostNameOrIP string) bool {
|
||||
// Attempt to strip port if present; this also strips brackets for
|
||||
// IPv6 addresses with a port (e.g. "[::1]:5000").
|
||||
//
|
||||
// This is best-effort; we'll continue using the address as-is if it fails.
|
||||
if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil {
|
||||
hostNameOrIP = host
|
||||
}
|
||||
if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") {
|
||||
// Fast path; no need to resolve these, assuming nobody overrides
|
||||
// "localhost" for anything else than a loopback address (sorry, not sorry).
|
||||
return true
|
||||
}
|
||||
|
||||
var addresses []net.IP
|
||||
if ip := net.ParseIP(hostNameOrIP); ip != nil {
|
||||
addresses = append(addresses, ip)
|
||||
} else {
|
||||
// Try to resolve the host's IP-addresses.
|
||||
addrs, _ := lookupIP(hostNameOrIP)
|
||||
addresses = append(addresses, addrs...)
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
if addr.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package registry
|
||||
|
||||
// defaultCertsDir is the platform-specific default directory where certificates
|
||||
// are stored. On Linux, it may be overridden through certsDir, for example, when
|
||||
// running in rootless mode.
|
||||
const defaultCertsDir = "/etc/docker/certs.d"
|
||||
|
||||
// cleanPath is used to ensure that a directory name is valid on the target
|
||||
// platform. It will be passed in something *similar* to a URL such as
|
||||
// https:/index.docker.io/v1. Not all platforms support directory names
|
||||
// which contain those characters (such as : on Windows)
|
||||
func cleanPath(s string) string {
|
||||
return s
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// defaultCertsDir is the platform-specific default directory where certificates
|
||||
// are stored. On Linux, it may be overridden through certsDir, for example, when
|
||||
// running in rootless mode.
|
||||
var defaultCertsDir = os.Getenv("programdata") + `\docker\certs.d`
|
||||
|
||||
// cleanPath is used to ensure that a directory name is valid on the target
|
||||
// platform. It will be passed in something *similar* to a URL such as
|
||||
// https:\index.docker.io\v1. Not all platforms support directory names
|
||||
// which contain those characters (such as : on Windows)
|
||||
func cleanPath(s string) string {
|
||||
return filepath.FromSlash(strings.ReplaceAll(s, ":", ""))
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func translateV2AuthError(err error) error {
|
||||
switch e := err.(type) {
|
||||
case *url.Error:
|
||||
switch e2 := e.Err.(type) {
|
||||
case errcode.Error:
|
||||
switch e2.Code {
|
||||
case errcode.ErrorCodeUnauthorized:
|
||||
return unauthorizedErr{err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
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...)}
|
||||
}
|
||||
|
||||
type unauthorizedErr struct{ error }
|
||||
|
||||
func (unauthorizedErr) Unauthorized() {}
|
||||
|
||||
func (e unauthorizedErr) Cause() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func (e unauthorizedErr) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
type invalidParameterErr struct{ error }
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/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 ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return ®istry.IndexInfo{
|
||||
Name: indexName,
|
||||
Mirrors: []string{},
|
||||
Secure: !isInsecure(indexName),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/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.
|
||||
return nil, invalidParamf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %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; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
|
||||
}
|
||||
|
||||
// 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(transport http.RoundTripper) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
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
|
||||
}
|
||||
|
|
@ -1,247 +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"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/docker/docker/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(fmt.Sprintf("%d", 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 := ®istry.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
|
||||
}
|
||||
|
|
@ -1,174 +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/docker/docker/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
|
||||
}
|
||||
|
||||
// ResolveRepository splits a repository name into its components
|
||||
// and configuration of the associated registry.
|
||||
//
|
||||
// Deprecated: this function was only used internally and is no longer used. It will be removed in the next release.
|
||||
func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
// TODO(thaJeztah): remove error return as it's no longer used.
|
||||
return newRepositoryInfo(s.config, name), nil
|
||||
}
|
||||
|
||||
// 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 = ®istry.IndexInfo{Name: indexName}
|
||||
}
|
||||
return ResolveAuthConfig(authConfigs, registryInfo)
|
||||
}
|
||||
|
||||
// APIEndpoint represents a remote API endpoint
|
||||
type APIEndpoint struct {
|
||||
Mirror bool
|
||||
URL *url.URL
|
||||
AllowNondistributableArtifacts bool // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release.
|
||||
Official bool // Deprecated: this field was only used internally, and will be removed in the next release.
|
||||
TrimHostname bool // Deprecated: hostname is now trimmed unconditionally for remote names. This field will be removed in the next release.
|
||||
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)
|
||||
}
|
||||
|
|
@ -1,74 +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,
|
||||
Official: true,
|
||||
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,24 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
)
|
||||
|
||||
// RepositoryInfo describes a repository
|
||||
type RepositoryInfo struct {
|
||||
Name reference.Named
|
||||
// Index points to registry information
|
||||
Index *registry.IndexInfo
|
||||
// Official indicates whether the repository is considered official.
|
||||
// If the registry is official, and the normalized name does not
|
||||
// contain a '/' (e.g. "foo"), then it is considered an official repo.
|
||||
//
|
||||
// Deprecated: this field is no longer used and will be removed in the next release. The information captured in this field can be obtained from the [Name] field instead.
|
||||
Official bool
|
||||
// Class represents the class of the repository, such as "plugin"
|
||||
// or "image".
|
||||
//
|
||||
// Deprecated: this field is no longer used, and will be removed in the next release.
|
||||
Class string
|
||||
}
|
||||
|
|
@ -92,12 +92,10 @@ github.com/docker/docker/api/types/volume
|
|||
github.com/docker/docker/client
|
||||
github.com/docker/docker/internal/lazyregexp
|
||||
github.com/docker/docker/internal/multierror
|
||||
github.com/docker/docker/pkg/homedir
|
||||
github.com/docker/docker/pkg/jsonmessage
|
||||
github.com/docker/docker/pkg/progress
|
||||
github.com/docker/docker/pkg/stdcopy
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue