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/command/completion"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/internal/tui"
|
"github.com/docker/cli/internal/tui"
|
||||||
"github.com/docker/docker/api/types/auxprogress"
|
"github.com/docker/docker/api/types/auxprogress"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/morikuni/aec"
|
"github.com/morikuni/aec"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"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
|
// Resolve the Repository name from fqn to RepositoryInfo
|
||||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
indexInfo := registry.NewIndexInfo(ref)
|
||||||
|
|
||||||
// Resolve the Auth config relevant for this server
|
// 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)
|
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -134,7 +134,7 @@ To push the complete multi-platform image, remove the --platform flag.
|
||||||
defer responseBody.Close()
|
defer responseBody.Close()
|
||||||
if !opts.untrusted {
|
if !opts.untrusted {
|
||||||
// TODO pushTrustedReference currently doesn't respect `--quiet`
|
// 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 {
|
if opts.quiet {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"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.
|
// 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())
|
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/cli/command/image"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/cli/internal/prompt"
|
"github.com/docker/cli/internal/prompt"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
@ -65,8 +65,7 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti
|
||||||
return types.PluginInstallOptions{}, err
|
return types.PluginInstallOptions{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
indexInfo := registry.NewIndexInfo(ref)
|
||||||
|
|
||||||
remote := ref.String()
|
remote := ref.String()
|
||||||
|
|
||||||
_, isCanonical := ref.(reference.Canonical)
|
_, isCanonical := ref.(reference.Canonical)
|
||||||
|
|
@ -84,7 +83,7 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti
|
||||||
remote = reference.FamiliarString(trusted)
|
remote = reference.FamiliarString(trusted)
|
||||||
}
|
}
|
||||||
|
|
||||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index)
|
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo)
|
||||||
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.PluginInstallOptions{}, err
|
return types.PluginInstallOptions{}, err
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/trust"
|
"github.com/docker/cli/cli/trust"
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -49,8 +49,8 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
||||||
|
|
||||||
named = reference.TagNameOnly(named)
|
named = reference.TagNameOnly(named)
|
||||||
|
|
||||||
repoInfo, _ := registry.ParseRepositoryInfo(named)
|
indexInfo := registry.NewIndexInfo(named)
|
||||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index)
|
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo)
|
||||||
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -63,6 +63,10 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
||||||
defer responseBody.Close()
|
defer responseBody.Close()
|
||||||
|
|
||||||
if !opts.untrusted {
|
if !opts.untrusted {
|
||||||
|
repoInfo := &trust.RepositoryInfo{
|
||||||
|
Name: reference.TrimNamed(named),
|
||||||
|
Index: indexInfo,
|
||||||
|
}
|
||||||
return trust.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody, command.UserAgent())
|
return trust.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody, command.UserAgent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@ import (
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
configtypes "github.com/docker/cli/cli/config/types"
|
configtypes "github.com/docker/cli/cli/config/types"
|
||||||
"github.com/docker/cli/internal/oauth/manager"
|
"github.com/docker/cli/internal/oauth/manager"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/internal/tui"
|
"github.com/docker/cli/internal/tui"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
@ -288,7 +288,7 @@ func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (*regis
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, token, err := svc.Auth(ctx, &auth, command.UserAgent())
|
token, err := svc.Auth(ctx, &auth, command.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ import (
|
||||||
configtypes "github.com/docker/cli/cli/config/types"
|
configtypes "github.com/docker/cli/cli/config/types"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/prompt"
|
"github.com/docker/cli/internal/prompt"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/api/types/system"
|
"github.com/docker/docker/api/types/system"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/fs"
|
"gotest.tools/v3/fs"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/config/credentials"
|
"github.com/docker/cli/cli/config/credentials"
|
||||||
"github.com/docker/cli/internal/oauth/manager"
|
"github.com/docker/cli/internal/oauth/manager"
|
||||||
"github.com/docker/docker/registry"
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ package registry
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/spf13/cobra"
|
"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") {
|
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.`)
|
_, _ = 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)
|
encodedAuth, err := getAuth(dockerCli, options.term)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo)
|
|
||||||
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -80,3 +74,37 @@ func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions
|
||||||
}
|
}
|
||||||
return SearchWrite(searchCtx, results)
|
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/distribution/reference"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/trust"
|
"github.com/docker/cli/cli/trust"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"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) {
|
func trustedResolveDigest(cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) {
|
||||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
indexInfo := registry.NewIndexInfo(ref)
|
||||||
authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index)
|
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")
|
notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "error establishing connection to trust repository")
|
return nil, errors.Wrap(err, "error establishing connection to trust repository")
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ import (
|
||||||
"github.com/docker/cli/cli/debug"
|
"github.com/docker/cli/cli/debug"
|
||||||
flagsHelper "github.com/docker/cli/cli/flags"
|
flagsHelper "github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/cli/internal/lazyregexp"
|
"github.com/docker/cli/internal/lazyregexp"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/templates"
|
"github.com/docker/cli/templates"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/api/types/system"
|
"github.com/docker/docker/api/types/system"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ var sampleInfoNoSwarm = system.Info{
|
||||||
IndexConfigs: map[string]*registrytypes.IndexInfo{
|
IndexConfigs: map[string]*registrytypes.IndexInfo{
|
||||||
"docker.io": {
|
"docker.io": {
|
||||||
Name: "docker.io",
|
Name: "docker.io",
|
||||||
Mirrors: nil,
|
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/distribution/registry/client/auth"
|
"github.com/docker/distribution/registry/client/auth"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,8 +34,7 @@ func (r repositoryEndpoint) BaseURL() string {
|
||||||
|
|
||||||
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
|
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
|
||||||
repoName := reference.TrimNamed(ref)
|
repoName := reference.TrimNamed(ref)
|
||||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
indexInfo := registry.NewIndexInfo(ref)
|
||||||
indexInfo := repoInfo.Index
|
|
||||||
|
|
||||||
endpoint, err := getDefaultEndpoint(ref, !indexInfo.Secure)
|
endpoint, err := getDefaultEndpoint(ref, !indexInfo.Secure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -54,7 +55,7 @@ func getDefaultEndpoint(repoName reference.Named, insecure bool) (registry.APIEn
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return registry.APIEndpoint{}, err
|
return registry.APIEndpoint{}, err
|
||||||
}
|
}
|
||||||
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoName))
|
endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(repoName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return registry.APIEndpoint{}, err
|
return registry.APIEndpoint{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +98,7 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API
|
||||||
if len(actions) == 0 {
|
if len(actions) == 0 {
|
||||||
actions = []string{"pull"}
|
actions = []string{"pull"}
|
||||||
}
|
}
|
||||||
creds := registry.NewStaticCredentialStore(&authConfig)
|
creds := &staticCredentialStore{authConfig: &authConfig}
|
||||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
|
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
|
||||||
basicHandler := auth.NewBasicHandler(creds)
|
basicHandler := auth.NewBasicHandler(creds)
|
||||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
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 {
|
func (*existingTokenHandler) Scheme() string {
|
||||||
return "bearer"
|
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/distribution/reference"
|
||||||
"github.com/docker/cli/cli/manifest/types"
|
"github.com/docker/cli/cli/manifest/types"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/manifest/manifestlist"
|
"github.com/docker/distribution/manifest/manifestlist"
|
||||||
"github.com/docker/distribution/manifest/ocischema"
|
"github.com/docker/distribution/manifest/ocischema"
|
||||||
|
|
@ -13,7 +14,6 @@ import (
|
||||||
"github.com/docker/distribution/registry/api/errcode"
|
"github.com/docker/distribution/registry/api/errcode"
|
||||||
v2 "github.com/docker/distribution/registry/api/v2"
|
v2 "github.com/docker/distribution/registry/api/v2"
|
||||||
distclient "github.com/docker/distribution/registry/client"
|
distclient "github.com/docker/distribution/registry/client"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
@ -221,8 +221,7 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
|
||||||
}
|
}
|
||||||
|
|
||||||
repoName := reference.TrimNamed(namedRef)
|
repoName := reference.TrimNamed(namedRef)
|
||||||
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
|
indexInfo := registry.NewIndexInfo(namedRef)
|
||||||
indexInfo := repoInfo.Index
|
|
||||||
|
|
||||||
confirmedTLSRegistries := make(map[string]bool)
|
confirmedTLSRegistries := make(map[string]bool)
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
|
|
@ -283,10 +282,9 @@ func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoi
|
||||||
}
|
}
|
||||||
registryService, err := registry.NewService(serviceOpts)
|
registryService, err := registry.NewService(serviceOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []registry.APIEndpoint{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
|
endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(namedRef))
|
||||||
endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
|
|
||||||
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
|
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
|
||||||
return endpoints, err
|
return endpoints, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@ import (
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/cli/cli/config"
|
"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"
|
||||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/go-connections/tlsconfig"
|
"github.com/docker/go-connections/tlsconfig"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
@ -95,7 +95,7 @@ func (simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {}
|
||||||
// GetNotaryRepository returns a NotaryRepository which stores all the
|
// GetNotaryRepository returns a NotaryRepository which stores all the
|
||||||
// information needed to operate on a notary repository.
|
// information needed to operate on a notary repository.
|
||||||
// It creates an HTTP transport providing authentication support.
|
// 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)
|
server, err := Server(repoInfo.Index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -304,11 +304,18 @@ type ImageRefAndAuth struct {
|
||||||
original string
|
original string
|
||||||
authConfig *registrytypes.AuthConfig
|
authConfig *registrytypes.AuthConfig
|
||||||
reference reference.Named
|
reference reference.Named
|
||||||
repoInfo *registry.RepositoryInfo
|
repoInfo *RepositoryInfo
|
||||||
tag string
|
tag string
|
||||||
digest digest.Digest
|
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
|
// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name
|
||||||
// as an ImageRefAndAuth struct
|
// as an ImageRefAndAuth struct
|
||||||
func GetImageReferencesAndAuth(ctx context.Context,
|
func GetImageReferencesAndAuth(ctx context.Context,
|
||||||
|
|
@ -321,13 +328,16 @@ func GetImageReferencesAndAuth(ctx context.Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the Repository name from fqn to RepositoryInfo
|
// Resolve the Repository name from fqn to RepositoryInfo
|
||||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
indexInfo := registry.NewIndexInfo(ref)
|
||||||
authConfig := authResolver(ctx, repoInfo.Index)
|
authConfig := authResolver(ctx, indexInfo)
|
||||||
return ImageRefAndAuth{
|
return ImageRefAndAuth{
|
||||||
original: imgName,
|
original: imgName,
|
||||||
authConfig: &authConfig,
|
authConfig: &authConfig,
|
||||||
reference: ref,
|
reference: ref,
|
||||||
repoInfo: repoInfo,
|
repoInfo: &RepositoryInfo{
|
||||||
|
Name: reference.TrimNamed(ref),
|
||||||
|
Index: indexInfo,
|
||||||
|
},
|
||||||
tag: getTag(ref),
|
tag: getTag(ref),
|
||||||
digest: getDigest(ref),
|
digest: getDigest(ref),
|
||||||
}, nil
|
}, nil
|
||||||
|
|
@ -366,7 +376,7 @@ func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepoInfo returns the repository information for a given ImageRefAndAuth
|
// RepoInfo returns the repository information for a given ImageRefAndAuth
|
||||||
func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo {
|
func (imgRefAuth *ImageRefAndAuth) RepoInfo() *RepositoryInfo {
|
||||||
return imgRefAuth.repoInfo
|
return imgRefAuth.repoInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"github.com/docker/cli/internal/jsonstream"
|
"github.com/docker/cli/internal/jsonstream"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/theupdateframework/notary/client"
|
"github.com/theupdateframework/notary/client"
|
||||||
|
|
@ -32,7 +31,7 @@ type Streams interface {
|
||||||
// PushTrustedReference pushes a canonical reference to the trust server.
|
// PushTrustedReference pushes a canonical reference to the trust server.
|
||||||
//
|
//
|
||||||
//nolint:gocyclo
|
//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
|
// 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.
|
// tag provided in the function and then do an AddTarget later.
|
||||||
notaryTarget := &client.Target{}
|
notaryTarget := &client.Target{}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import (
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/internal/oauth"
|
"github.com/docker/cli/internal/oauth"
|
||||||
"github.com/docker/cli/internal/oauth/api"
|
"github.com/docker/cli/internal/oauth/api"
|
||||||
|
"github.com/docker/cli/internal/registry"
|
||||||
"github.com/docker/cli/internal/tui"
|
"github.com/docker/cli/internal/tui"
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/morikuni/aec"
|
"github.com/morikuni/aec"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -12,7 +13,6 @@ import (
|
||||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthClientID is used the ClientID used for the token server
|
// 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
|
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
|
// loginV2 tries to login to the v2 registry server. The given registry
|
||||||
// endpoint will be pinged to get authorization challenges. These challenges
|
// endpoint will be pinged to get authorization challenges. These challenges
|
||||||
// will be used to authenticate against the registry to validate credentials.
|
// 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 {
|
if resp.StatusCode != http.StatusOK {
|
||||||
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
|
// 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
|
return credentialAuthConfig.IdentityToken, nil
|
||||||
|
|
@ -127,64 +98,19 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi
|
||||||
}, nil
|
}, 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
|
// PingV2Registry attempts to ping a v2 registry and on success return a
|
||||||
// challenge manager for the supported authentication types.
|
// challenge manager for the supported authentication types.
|
||||||
// If a response is received but cannot be interpreted, a PingResponseError will be returned.
|
// 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) {
|
func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
|
||||||
pingClient := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Timeout: 15 * time.Second,
|
|
||||||
}
|
|
||||||
endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
|
endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
|
||||||
req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody)
|
req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
pingClient := &http.Client{
|
||||||
|
Transport: authTransport,
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
}
|
||||||
resp, err := pingClient.Do(req)
|
resp, err := pingClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -193,9 +119,7 @@ func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.M
|
||||||
|
|
||||||
challengeManager := challenge.NewSimpleManager()
|
challengeManager := challenge.NewSimpleManager()
|
||||||
if err := challengeManager.AddResponse(resp); err != nil {
|
if err := challengeManager.AddResponse(resp); err != nil {
|
||||||
return nil, PingResponseError{
|
return nil, err
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return challengeManager, nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containerd/log"
|
"github.com/containerd/log"
|
||||||
|
|
@ -16,16 +19,15 @@ import (
|
||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
"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.
|
// hostCertsDir returns the config directory for a specific host.
|
||||||
func hostCertsDir(hostname string) string {
|
func hostCertsDir(hostnameAndPort string) string {
|
||||||
return filepath.Join(CertsDir(), cleanPath(hostname))
|
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
|
// 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 {
|
if tlsConfig.RootCAs == nil {
|
||||||
systemPool, err := tlsconfig.SystemCertPool()
|
systemPool, err := tlsconfig.SystemCertPool()
|
||||||
if err != nil {
|
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
|
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 (
|
require (
|
||||||
dario.cat/mergo v1.0.1
|
dario.cat/mergo v1.0.1
|
||||||
github.com/containerd/errdefs v1.0.0
|
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/containerd/platforms v1.0.0-rc.1
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
|
|
@ -47,6 +48,7 @@ require (
|
||||||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
|
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
|
||||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0
|
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 v1.35.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc 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
|
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/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.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 v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||||
github.com/docker/go-metrics v0.0.1 // 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
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
|
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // 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 v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp 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
|
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/client
|
||||||
github.com/docker/docker/internal/lazyregexp
|
github.com/docker/docker/internal/lazyregexp
|
||||||
github.com/docker/docker/internal/multierror
|
github.com/docker/docker/internal/multierror
|
||||||
github.com/docker/docker/pkg/homedir
|
|
||||||
github.com/docker/docker/pkg/jsonmessage
|
github.com/docker/docker/pkg/jsonmessage
|
||||||
github.com/docker/docker/pkg/progress
|
github.com/docker/docker/pkg/progress
|
||||||
github.com/docker/docker/pkg/stdcopy
|
github.com/docker/docker/pkg/stdcopy
|
||||||
github.com/docker/docker/pkg/streamformatter
|
github.com/docker/docker/pkg/streamformatter
|
||||||
github.com/docker/docker/registry
|
|
||||||
# github.com/docker/docker-credential-helpers v0.9.3
|
# github.com/docker/docker-credential-helpers v0.9.3
|
||||||
## explicit; go 1.21
|
## explicit; go 1.21
|
||||||
github.com/docker/docker-credential-helpers/client
|
github.com/docker/docker-credential-helpers/client
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue