
Support CRI configuration to allow for request-time rewrite rules applicable only to the repository portion of resource paths when pulling images. Because the rewrites are applied at request time, images themselves will not be "rewritten" -- images as stored by CRI (and the underlying containerd facility) will continue to present as normal. As an example, if you use the following config for your containerd: ```toml [plugins] [plugins."io.containerd.grpc.v1.cri"] [plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://registry-1.docker.io/v2"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io".rewrite] "^library/(.*)" = "my-org/$1" ``` And then subsequently invoke `crictl pull alpine:3.13` it will pull content from `docker.io/my-org/alpine:3.13` but still show up as `docker.io/library/alpine:3.13` in the `crictl images` listing. This commit has been reworked from the original implementation. Rewites are now done when resolving instead of when building the request, so that auth token scopes stored in the context properly reflect the rewritten repository path. For the original implementation, see 06c4ea9baec2b278b8172a789bf601168292f645. Ref: https://github.com/k3s-io/k3s/issues/11191#issuecomment-2455525773 Signed-off-by: Jacob Blain Christen <jacob@rancher.com> Co-authored-by: Brad Davidson <brad.davidson@rancher.com> Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
246 lines
6.8 KiB
Go
246 lines
6.8 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package docker
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
)
|
|
|
|
// HostCapabilities represent the capabilities of the registry
|
|
// host. This also represents the set of operations for which
|
|
// the registry host may be trusted to perform.
|
|
//
|
|
// For example pushing is a capability which should only be
|
|
// performed on an upstream source, not a mirror.
|
|
// Resolving (the process of converting a name into a digest)
|
|
// must be considered a trusted operation and only done by
|
|
// a host which is trusted (or more preferably by secure process
|
|
// which can prove the provenance of the mapping). A public
|
|
// mirror should never be trusted to do a resolve action.
|
|
//
|
|
// | Registry Type | Pull | Resolve | Push |
|
|
// |------------------|------|---------|------|
|
|
// | Public Registry | yes | yes | yes |
|
|
// | Private Registry | yes | yes | yes |
|
|
// | Public Mirror | yes | no | no |
|
|
// | Private Mirror | yes | yes | no |
|
|
type HostCapabilities uint8
|
|
|
|
const (
|
|
// HostCapabilityPull represents the capability to fetch manifests
|
|
// and blobs by digest
|
|
HostCapabilityPull HostCapabilities = 1 << iota
|
|
|
|
// HostCapabilityResolve represents the capability to fetch manifests
|
|
// by name
|
|
HostCapabilityResolve
|
|
|
|
// HostCapabilityPush represents the capability to push blobs and
|
|
// manifests
|
|
HostCapabilityPush
|
|
|
|
// Reserved for future capabilities (i.e. search, catalog, remove)
|
|
)
|
|
|
|
// Has checks whether the capabilities list has the provide capability
|
|
func (c HostCapabilities) Has(t HostCapabilities) bool {
|
|
return c&t == t
|
|
}
|
|
|
|
// RegistryHost represents a complete configuration for a registry
|
|
// host, representing the capabilities, authorizations, connection
|
|
// configuration, and location.
|
|
type RegistryHost struct {
|
|
Client *http.Client
|
|
Authorizer Authorizer
|
|
Host string
|
|
Scheme string
|
|
Path string
|
|
Capabilities HostCapabilities
|
|
Header http.Header
|
|
Rewrites map[string]string
|
|
}
|
|
|
|
func (h RegistryHost) isProxy(refhost string) bool {
|
|
if refhost != h.Host {
|
|
if refhost != "docker.io" || h.Host != "registry-1.docker.io" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RegistryHosts fetches the registry hosts for a given namespace,
|
|
// provided by the host component of an distribution image reference.
|
|
type RegistryHosts func(string) ([]RegistryHost, error)
|
|
|
|
// Registries joins multiple registry configuration functions, using the same
|
|
// order as provided within the arguments. When an empty registry configuration
|
|
// is returned with a nil error, the next function will be called.
|
|
// NOTE: This function will not join configurations, as soon as a non-empty
|
|
// configuration is returned from a configuration function, it will be returned
|
|
// to the caller.
|
|
func Registries(registries ...RegistryHosts) RegistryHosts {
|
|
return func(host string) ([]RegistryHost, error) {
|
|
for _, registry := range registries {
|
|
config, err := registry(host)
|
|
if err != nil {
|
|
return config, err
|
|
}
|
|
if len(config) > 0 {
|
|
return config, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
type registryOpts struct {
|
|
authorizer Authorizer
|
|
plainHTTP func(string) (bool, error)
|
|
host func(string) (string, error)
|
|
client *http.Client
|
|
}
|
|
|
|
// RegistryOpt defines a registry default option
|
|
type RegistryOpt func(*registryOpts)
|
|
|
|
// WithPlainHTTP configures registries to use plaintext http scheme
|
|
// for the provided host match function.
|
|
func WithPlainHTTP(f func(string) (bool, error)) RegistryOpt {
|
|
return func(opts *registryOpts) {
|
|
opts.plainHTTP = f
|
|
}
|
|
}
|
|
|
|
// WithAuthorizer configures the default authorizer for a registry
|
|
func WithAuthorizer(a Authorizer) RegistryOpt {
|
|
return func(opts *registryOpts) {
|
|
opts.authorizer = a
|
|
}
|
|
}
|
|
|
|
// WithHostTranslator defines the default translator to use for registry hosts
|
|
func WithHostTranslator(h func(string) (string, error)) RegistryOpt {
|
|
return func(opts *registryOpts) {
|
|
opts.host = h
|
|
}
|
|
}
|
|
|
|
// WithClient configures the default http client for a registry
|
|
func WithClient(c *http.Client) RegistryOpt {
|
|
return func(opts *registryOpts) {
|
|
opts.client = c
|
|
}
|
|
}
|
|
|
|
// ConfigureDefaultRegistries is used to create a default configuration for
|
|
// registries. For more advanced configurations or per-domain setups,
|
|
// the RegistryHosts interface should be used directly.
|
|
// NOTE: This function will always return a non-empty value or error
|
|
func ConfigureDefaultRegistries(ropts ...RegistryOpt) RegistryHosts {
|
|
var opts registryOpts
|
|
for _, opt := range ropts {
|
|
opt(&opts)
|
|
}
|
|
|
|
return func(host string) ([]RegistryHost, error) {
|
|
config := RegistryHost{
|
|
Client: opts.client,
|
|
Authorizer: opts.authorizer,
|
|
Host: host,
|
|
Scheme: "https",
|
|
Path: "/v2",
|
|
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush,
|
|
}
|
|
|
|
if config.Client == nil {
|
|
config.Client = http.DefaultClient
|
|
}
|
|
|
|
if opts.plainHTTP != nil {
|
|
match, err := opts.plainHTTP(host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if match {
|
|
config.Scheme = "http"
|
|
}
|
|
}
|
|
|
|
if opts.host != nil {
|
|
var err error
|
|
config.Host, err = opts.host(config.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if host == "docker.io" {
|
|
config.Host = "registry-1.docker.io"
|
|
}
|
|
|
|
return []RegistryHost{config}, nil
|
|
}
|
|
}
|
|
|
|
// MatchAllHosts is a host match function which is always true.
|
|
func MatchAllHosts(string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
// MatchLocalhost is a host match function which returns true for
|
|
// localhost.
|
|
//
|
|
// Note: this does not handle matching of ip addresses in octal,
|
|
// decimal or hex form.
|
|
func MatchLocalhost(host string) (bool, error) {
|
|
switch {
|
|
case host == "::1":
|
|
return true, nil
|
|
case host == "[::1]":
|
|
return true, nil
|
|
}
|
|
h, p, err := net.SplitHostPort(host)
|
|
|
|
// addrError helps distinguish between errors of form
|
|
// "no colon in address" and "too many colons in address".
|
|
// The former is fine as the host string need not have a
|
|
// port. Latter needs to be handled.
|
|
addrError := &net.AddrError{
|
|
Err: "missing port in address",
|
|
Addr: host,
|
|
}
|
|
if err != nil {
|
|
if err.Error() != addrError.Error() {
|
|
return false, err
|
|
}
|
|
// host string without any port specified
|
|
h = host
|
|
} else if len(p) == 0 {
|
|
return false, errors.New("invalid host name format")
|
|
}
|
|
|
|
// use ipv4 dotted decimal for further checking
|
|
if h == "localhost" {
|
|
h = "127.0.0.1"
|
|
}
|
|
ip := net.ParseIP(h)
|
|
|
|
return ip.IsLoopback(), nil
|
|
}
|