Use containerd registry mirror library.

Signed-off-by: Lantao Liu <lantaol@google.com>
This commit is contained in:
Lantao Liu 2019-08-06 18:19:40 -07:00
parent 27de1a5862
commit 53e94c6753
5 changed files with 185 additions and 133 deletions

13
cri.go
View File

@ -170,6 +170,19 @@ func validateConfig(ctx context.Context, c *criconfig.Config) error {
} }
} }
// Validation for registry configurations.
if len(c.Registry.Auths) != 0 {
if c.Registry.Configs == nil {
c.Registry.Configs = make(map[string]criconfig.RegistryConfig)
}
for endpoint, auth := range c.Registry.Auths {
config := c.Registry.Configs[endpoint]
config.Auth = &auth
c.Registry.Configs[endpoint] = config
}
log.G(ctx).Warning("`auths` is deprecated, please use registry`configs` instead")
}
// Validation for stream_idle_timeout // Validation for stream_idle_timeout
if c.StreamIdleTimeout != "" { if c.StreamIdleTimeout != "" {
if _, err := time.ParseDuration(c.StreamIdleTimeout); err != nil { if _, err := time.ParseDuration(c.StreamIdleTimeout); err != nil {

View File

@ -27,8 +27,8 @@ After modify this config, you need restart the `containerd` service.
To configure the TLS settings for a specific registry, create/modify the `/etc/containerd/config.toml` as follows: To configure the TLS settings for a specific registry, create/modify the `/etc/containerd/config.toml` as follows:
```toml ```toml
[plugins.cri.registry.tls_configs] # The registry host has to be an FDQN or IP.
[plugins.cri.registry.tls_configs."my.custom.registry"] [plugins.cri.registry.configs."my.custom.registry".tls]
ca_file = "ca.pem" ca_file = "ca.pem"
cert_file = "cert.pem" cert_file = "cert.pem"
key_file = "key.pem" key_file = "key.pem"
@ -37,16 +37,15 @@ To configure the TLS settings for a specific registry, create/modify the `/etc/c
In the config example shown above, TLS mutual authentication will be used for communications with the registry endpoint located at https://my.custom.registry. In the config example shown above, TLS mutual authentication will be used for communications with the registry endpoint located at https://my.custom.registry.
`ca_file` is file name of the certificate authority (CA) certificate used to authenticate the x509 certificate/key pair specified by the files respectively pointed to by `cert_file` and `key_file`. `ca_file` is file name of the certificate authority (CA) certificate used to authenticate the x509 certificate/key pair specified by the files respectively pointed to by `cert_file` and `key_file`.
## Configure Registry Credentials ## Configure Registry Credentials
`cri` plugin also supports docker like registry credential config. `cri` plugin also supports docker like registry credential config.
To configure a credential for a specific registry endpoint, create/modify the To configure a credential for a specific registry, create/modify the
`/etc/containerd/config.toml` as follows: `/etc/containerd/config.toml` as follows:
```toml ```toml
[plugins.cri.registry.auths] # The registry host has to be an FDQN or IP.
[plugins.cri.registry.auths."https://gcr.io"] [plugins.cri.registry.configs."gcr.io".auth]
username = "" username = ""
password = "" password = ""
auth = "" auth = ""

View File

@ -95,6 +95,7 @@ type Mirror struct {
// Endpoints are endpoints for a namespace. CRI plugin will try the endpoints // Endpoints are endpoints for a namespace. CRI plugin will try the endpoints
// one by one until a working one is found. The endpoint must be a valid url // one by one until a working one is found. The endpoint must be a valid url
// with host specified. // with host specified.
// The scheme, host and path from the endpoint URL will be used.
Endpoints []string `toml:"endpoint" json:"endpoint"` Endpoints []string `toml:"endpoint" json:"endpoint"`
} }
@ -123,12 +124,23 @@ type TLSConfig struct {
type Registry struct { type Registry struct {
// Mirrors are namespace to mirror mapping for all namespaces. // Mirrors are namespace to mirror mapping for all namespaces.
Mirrors map[string]Mirror `toml:"mirrors" json:"mirrors"` Mirrors map[string]Mirror `toml:"mirrors" json:"mirrors"`
// Configs are configs for each registry.
// The key is the FDQN or IP of the registry.
Configs map[string]RegistryConfig `toml:"configs" json:"configs"`
// Auths are registry endpoint to auth config mapping. The registry endpoint must // Auths are registry endpoint to auth config mapping. The registry endpoint must
// be a valid url with host specified. // be a valid url with host specified.
// DEPRECATED: Use Configs instead. Remove in containerd 1.4.
Auths map[string]AuthConfig `toml:"auths" json:"auths"` Auths map[string]AuthConfig `toml:"auths" json:"auths"`
// TLSConfigs are pairs of CA/Cert/Key which then are used when creating the transport }
// RegistryConfig contains configuration used to communicate with the registry.
type RegistryConfig struct {
// Auth contains information to authenticate to the registry.
Auth *AuthConfig `toml:"auth" json:"auth"`
// TLS is a pair of CA/Cert/Key which then are used when creating the transport
// that communicates with the registry. // that communicates with the registry.
TLSConfigs map[string]TLSConfig `toml:"tls_configs" json:"tlsConfigs"` TLS *TLSConfig `toml:"tls" json:"tls"`
} }
// PluginConfig contains toml config related to CRI plugin, // PluginConfig contains toml config related to CRI plugin,

View File

@ -31,16 +31,15 @@ import (
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
containerdimages "github.com/containerd/containerd/images" containerdimages "github.com/containerd/containerd/images"
"github.com/containerd/containerd/log" "github.com/containerd/containerd/log"
"github.com/containerd/containerd/reference"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker" "github.com/containerd/containerd/remotes/docker"
"github.com/containerd/cri/pkg/util"
distribution "github.com/docker/distribution/reference" distribution "github.com/docker/distribution/reference"
imagespec "github.com/opencontainers/image-spec/specs-go/v1" imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/net/context" "golang.org/x/net/context"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
"github.com/containerd/cri/pkg/config" criconfig "github.com/containerd/cri/pkg/config"
) )
// For image management: // For image management:
@ -95,7 +94,10 @@ func (c *criService) PullImage(ctx context.Context, r *runtime.PullImageRequest)
if ref != imageRef { if ref != imageRef {
log.G(ctx).Debugf("PullImage using normalized image ref: %q", ref) log.G(ctx).Debugf("PullImage using normalized image ref: %q", ref)
} }
resolver, desc, err := c.getResolver(ctx, ref, c.credentials(r.GetAuth())) resolver := docker.NewResolver(docker.ResolverOptions{
Hosts: c.registryHosts(r.GetAuth()),
})
_, desc, err := resolver.Resolve(ctx, ref)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to resolve image %q", ref) return nil, errors.Wrapf(err, "failed to resolve image %q", ref)
} }
@ -148,10 +150,20 @@ func (c *criService) PullImage(ctx context.Context, r *runtime.PullImageRequest)
} }
// ParseAuth parses AuthConfig and returns username and password/secret required by containerd. // ParseAuth parses AuthConfig and returns username and password/secret required by containerd.
func ParseAuth(auth *runtime.AuthConfig) (string, string, error) { func ParseAuth(auth *runtime.AuthConfig, host string) (string, string, error) {
if auth == nil { if auth == nil {
return "", "", nil return "", "", nil
} }
if auth.ServerAddress != "" {
// Do not return the auth info when server address doesn't match.
u, err := url.Parse(auth.ServerAddress)
if err != nil {
return "", "", errors.Wrap(err, "parse server address")
}
if host != u.Host {
return "", "", nil
}
}
if auth.Username != "" { if auth.Username != "" {
return auth.Username, auth.Password, nil return auth.Username, auth.Password, nil
} }
@ -235,28 +247,8 @@ func (c *criService) updateImage(ctx context.Context, r string) error {
return nil return nil
} }
// credentials returns a credential function for docker resolver to use.
func (c *criService) credentials(auth *runtime.AuthConfig) func(string) (string, string, error) {
return func(host string) (string, string, error) {
if auth == nil {
// Get default auth from config.
for h, ac := range c.config.Registry.Auths {
u, err := url.Parse(h)
if err != nil {
return "", "", errors.Wrapf(err, "parse auth host %q", h)
}
if u.Host == host {
auth = toRuntimeAuthConfig(ac)
break
}
}
}
return ParseAuth(auth)
}
}
// getTLSConfig returns a TLSConfig configured with a CA/Cert/Key specified by registryTLSConfig // getTLSConfig returns a TLSConfig configured with a CA/Cert/Key specified by registryTLSConfig
func (c *criService) getTLSConfig(registryTLSConfig config.TLSConfig) (*tls.Config, error) { func (c *criService) getTLSConfig(registryTLSConfig criconfig.TLSConfig) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(registryTLSConfig.CertFile, registryTLSConfig.KeyFile) cert, err := tls.LoadX509KeyPair(registryTLSConfig.CertFile, registryTLSConfig.KeyFile)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to load cert file") return nil, errors.Wrap(err, "failed to load cert file")
@ -280,68 +272,79 @@ func (c *criService) getTLSConfig(registryTLSConfig config.TLSConfig) (*tls.Conf
return tlsConfig, nil return tlsConfig, nil
} }
// getResolver tries registry mirrors and the default registry, and returns the resolver and descriptor // registryHosts is the registry hosts to be used by the resolver.
// from the first working registry. func (c *criService) registryHosts(auth *runtime.AuthConfig) docker.RegistryHosts {
func (c *criService) getResolver(ctx context.Context, ref string, cred func(string) (string, string, error)) (remotes.Resolver, imagespec.Descriptor, error) { return func(host string) ([]docker.RegistryHost, error) {
refspec, err := reference.Parse(ref) var registries []docker.RegistryHost
// Try mirrors in order, and then try the default registry if not tried.
endpoints, err := addDefaultEndpoint(
c.config.Registry.Mirrors[host].Endpoints, host)
if err != nil { if err != nil {
return nil, imagespec.Descriptor{}, errors.Wrap(err, "parse image reference") return nil, errors.Wrapf(err, "add default endpoint")
}
for _, e := range endpoints {
u, err := url.Parse(e)
if err != nil {
return nil, errors.Wrapf(err, "parse registry endpoint %q from mirrors", e)
} }
var ( var (
transport = newTransport() transport = newTransport()
httpClient = &http.Client{Transport: transport} client = &http.Client{Transport: transport}
config = c.config.Registry.Configs[u.Host]
) )
// Try mirrors in order first, and then try default host name. if u.Scheme != "https" && config.TLS != nil {
for _, e := range c.config.Registry.Mirrors[refspec.Hostname()].Endpoints { return nil, errors.Errorf("tls provided for http endpoint %q", e)
u, err := url.Parse(e)
if err != nil {
return nil, imagespec.Descriptor{}, errors.Wrapf(err, "parse registry endpoint %q", e)
} }
if registryTLSConfig, ok := c.config.Registry.TLSConfigs[u.Host]; ok { if config.TLS != nil {
transport.TLSClientConfig, err = c.getTLSConfig(registryTLSConfig) transport.TLSClientConfig, err = c.getTLSConfig(*config.TLS)
if err != nil { if err != nil {
return nil, imagespec.Descriptor{}, errors.Wrapf(err, "get TLSConfig for registry %q", refspec.Hostname()) return nil, errors.Wrapf(err, "get TLSConfig for registry %q", e)
} }
} }
resolver := docker.NewResolver(docker.ResolverOptions{ if auth == nil && config.Auth != nil {
Authorizer: docker.NewAuthorizer(httpClient, cred), auth = toRuntimeAuthConfig(*config.Auth)
Client: httpClient, }
Host: func(string) (string, error) { return u.Host, nil },
// By default use "https". if u.Path == "" {
PlainHTTP: u.Scheme == "http", u.Path = "/v2"
}
registries = append(registries, docker.RegistryHost{
Client: client,
Authorizer: docker.NewDockerAuthorizer(
docker.WithAuthClient(client),
docker.WithAuthCreds(func(host string) (string, string, error) {
return ParseAuth(auth, host)
})),
Host: u.Host,
Scheme: u.Scheme,
Path: u.Path,
Capabilities: docker.HostCapabilityResolve | docker.HostCapabilityPull,
}) })
_, desc, err := resolver.Resolve(ctx, ref)
if err == nil {
return resolver, desc, nil
} }
log.G(ctx).WithError(err).Debugf("Tried registry mirror %q but failed", e) return registries, nil
// Continue to try next endpoint
} }
}
hostname, err := docker.DefaultHost(refspec.Hostname()) // addDefaultEndpoint add default registry endpoint if it does not
// exist in the passed-in endpoint list.
func addDefaultEndpoint(endpoints []string, host string) ([]string, error) {
defaultHost, err := docker.DefaultHost(host)
if err != nil { if err != nil {
return nil, imagespec.Descriptor{}, errors.Wrapf(err, "get host for refspec %q", refspec.Hostname()) return nil, errors.Wrapf(err, "get default host")
} }
if registryTLSConfig, ok := c.config.Registry.TLSConfigs[hostname]; ok { // If the http endpoint is configured, do not try https.
transport.TLSClientConfig, err = c.getTLSConfig(registryTLSConfig) if !util.InStringSlice(endpoints, "http://"+defaultHost) {
if err != nil { if !util.InStringSlice(endpoints, "https://"+defaultHost) {
return nil, imagespec.Descriptor{}, errors.Wrapf(err, "get TLSConfig for registry %q", refspec.Hostname()) return append(endpoints, "https://"+defaultHost), nil
} }
} }
return endpoints, nil
resolver := docker.NewResolver(docker.ResolverOptions{
Credentials: cred,
Client: httpClient,
})
_, desc, err := resolver.Resolve(ctx, ref)
if err != nil {
return nil, imagespec.Descriptor{}, errors.Wrap(err, "no available registry endpoint")
}
return resolver, desc, nil
} }
// newTransport returns a new HTTP transport used to pull image. // newTransport returns a new HTTP transport used to pull image.

View File

@ -22,8 +22,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
criconfig "github.com/containerd/cri/pkg/config"
) )
func TestParseAuth(t *testing.T) { func TestParseAuth(t *testing.T) {
@ -36,6 +34,7 @@ func TestParseAuth(t *testing.T) {
base64.StdEncoding.Encode(invalidAuth, []byte(testUser+"@"+testPasswd)) base64.StdEncoding.Encode(invalidAuth, []byte(testUser+"@"+testPasswd))
for desc, test := range map[string]struct { for desc, test := range map[string]struct {
auth *runtime.AuthConfig auth *runtime.AuthConfig
host string
expectedUser string expectedUser string
expectedSecret string expectedSecret string
expectErr bool expectErr bool
@ -66,66 +65,92 @@ func TestParseAuth(t *testing.T) {
auth: &runtime.AuthConfig{Auth: string(invalidAuth)}, auth: &runtime.AuthConfig{Auth: string(invalidAuth)},
expectErr: true, expectErr: true,
}, },
"should return empty auth if server address doesn't match": {
auth: &runtime.AuthConfig{
Username: testUser,
Password: testPasswd,
ServerAddress: "https://registry-1.io",
},
host: "registry-2.io",
expectedUser: "",
expectedSecret: "",
},
"should return auth if server address matches": {
auth: &runtime.AuthConfig{
Username: testUser,
Password: testPasswd,
ServerAddress: "https://registry-1.io",
},
host: "registry-1.io",
expectedUser: testUser,
expectedSecret: testPasswd,
},
"should return auth if server address is not specified": {
auth: &runtime.AuthConfig{
Username: testUser,
Password: testPasswd,
},
host: "registry-1.io",
expectedUser: testUser,
expectedSecret: testPasswd,
},
} { } {
t.Logf("TestCase %q", desc) t.Logf("TestCase %q", desc)
u, s, err := ParseAuth(test.auth) u, s, err := ParseAuth(test.auth, test.host)
assert.Equal(t, test.expectErr, err != nil) assert.Equal(t, test.expectErr, err != nil)
assert.Equal(t, test.expectedUser, u) assert.Equal(t, test.expectedUser, u)
assert.Equal(t, test.expectedSecret, s) assert.Equal(t, test.expectedSecret, s)
} }
} }
func TestCredentials(t *testing.T) { func TestAddDefaultEndpoint(t *testing.T) {
c := newTestCRIService()
c.config.Registry.Auths = map[string]criconfig.AuthConfig{
"https://test1.io": {
Username: "username1",
Password: "password1",
},
"http://test2.io": {
Username: "username2",
Password: "password2",
},
"//test3.io": {
Username: "username3",
Password: "password3",
},
}
for desc, test := range map[string]struct { for desc, test := range map[string]struct {
auth *runtime.AuthConfig endpoints []string
host string host string
expectedUsername string expected []string
expectedPassword string
}{ }{
"auth config from CRI should take precedence": { "default endpoint not in list": {
auth: &runtime.AuthConfig{ endpoints: []string{
Username: "username", "https://registry-1.io",
Password: "password", "https://registry-2.io",
}, },
host: "test1.io", host: "registry-3.io",
expectedUsername: "username", expected: []string{
expectedPassword: "password", "https://registry-1.io",
"https://registry-2.io",
"https://registry-3.io",
}, },
"should support https host": {
host: "test1.io",
expectedUsername: "username1",
expectedPassword: "password1",
}, },
"should support http host": { "default endpoint in list with http": {
host: "test2.io", endpoints: []string{
expectedUsername: "username2", "https://registry-1.io",
expectedPassword: "password2", "https://registry-2.io",
"http://registry-3.io",
},
host: "registry-3.io",
expected: []string{
"https://registry-1.io",
"https://registry-2.io",
"http://registry-3.io",
},
},
"default endpoint in list with https": {
endpoints: []string{
"https://registry-1.io",
"https://registry-2.io",
"https://registry-3.io",
},
host: "registry-3.io",
expected: []string{
"https://registry-1.io",
"https://registry-2.io",
"https://registry-3.io",
}, },
"should support hostname only": {
host: "test3.io",
expectedUsername: "username3",
expectedPassword: "password3",
}, },
} { } {
t.Logf("TestCase %q", desc) t.Logf("TestCase %q", desc)
username, password, err := c.credentials(test.auth)(test.host) got, err := addDefaultEndpoint(test.endpoints, test.host)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, test.expectedUsername, username) assert.Equal(t, test.expected, got)
assert.Equal(t, test.expectedPassword, password)
} }
} }