Mirror repository rewrites (v1.1)
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>
This commit is contained in:
committed by
Brad Davidson
parent
b5eb7da8e5
commit
48894d6f5e
@@ -49,14 +49,14 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
|
||||
return nil, fmt.Errorf("no pull hosts: %w", errdefs.ErrNotFound)
|
||||
}
|
||||
|
||||
ctx, err := ContextWithRepositoryScope(ctx, r.refspec, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newHTTPReadSeeker(desc.Size, func(offset int64) (io.ReadCloser, error) {
|
||||
// firstly try fetch via external urls
|
||||
for _, us := range desc.URLs {
|
||||
ctx, err := ContextWithRepositoryScope(ctx, r.refspec, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, err := url.Parse(us)
|
||||
if err != nil {
|
||||
log.G(ctx).WithError(err).Debugf("failed to parse %q", us)
|
||||
@@ -102,8 +102,14 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
|
||||
|
||||
var firstErr error
|
||||
for _, host := range r.hosts {
|
||||
req := r.request(host, http.MethodGet, "manifests", desc.Digest.String())
|
||||
if err := req.addNamespace(r.refspec.Hostname()); err != nil {
|
||||
base := r.withRewritesFromHost(host)
|
||||
ctx, err := ContextWithRepositoryScope(ctx, base.refspec, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := base.request(host, http.MethodGet, "manifests", desc.Digest.String())
|
||||
if err := req.addNamespace(base.refspec.Hostname()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -125,8 +131,14 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
|
||||
// Finally use blobs endpoints
|
||||
var firstErr error
|
||||
for _, host := range r.hosts {
|
||||
req := r.request(host, http.MethodGet, "blobs", desc.Digest.String())
|
||||
if err := req.addNamespace(r.refspec.Hostname()); err != nil {
|
||||
base := r.withRewritesFromHost(host)
|
||||
ctx, err := ContextWithRepositoryScope(ctx, base.refspec, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := base.request(host, http.MethodGet, "blobs", desc.Digest.String())
|
||||
if err := req.addNamespace(base.refspec.Hostname()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -154,8 +166,14 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
|
||||
}
|
||||
|
||||
func (r dockerFetcher) createGetReq(ctx context.Context, host RegistryHost, mediatype string, ps ...string) (*request, int64, error) {
|
||||
headReq := r.request(host, http.MethodHead, ps...)
|
||||
if err := headReq.addNamespace(r.refspec.Hostname()); err != nil {
|
||||
base := r.withRewritesFromHost(host)
|
||||
ctx, err := ContextWithRepositoryScope(ctx, base.refspec, false)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
headReq := base.request(host, http.MethodHead, ps...)
|
||||
if err := headReq.addNamespace(base.refspec.Hostname()); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -176,8 +194,8 @@ func (r dockerFetcher) createGetReq(ctx context.Context, host RegistryHost, medi
|
||||
return nil, 0, fmt.Errorf("unexpected HEAD status code %v: %s", headReq.String(), headResp.Status)
|
||||
}
|
||||
|
||||
getReq := r.request(host, http.MethodGet, ps...)
|
||||
if err := getReq.addNamespace(r.refspec.Hostname()); err != nil {
|
||||
getReq := base.request(host, http.MethodGet, ps...)
|
||||
if err := getReq.addNamespace(base.refspec.Hostname()); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return getReq, headResp.ContentLength, nil
|
||||
@@ -198,15 +216,10 @@ func (r dockerFetcher) FetchByDigest(ctx context.Context, dgst digest.Digest, op
|
||||
return nil, desc, fmt.Errorf("no pull hosts: %w", errdefs.ErrNotFound)
|
||||
}
|
||||
|
||||
ctx, err := ContextWithRepositoryScope(ctx, r.refspec, false)
|
||||
if err != nil {
|
||||
return nil, desc, err
|
||||
}
|
||||
|
||||
var (
|
||||
getReq *request
|
||||
sz int64
|
||||
firstErr error
|
||||
getReq *request
|
||||
sz int64
|
||||
err, firstErr error
|
||||
)
|
||||
|
||||
for _, host := range r.hosts {
|
||||
|
||||
@@ -72,10 +72,6 @@ func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, ref str
|
||||
l.Lock(ref)
|
||||
defer l.Unlock(ref)
|
||||
}
|
||||
ctx, err := ContextWithRepositoryScope(ctx, p.refspec, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status, err := p.tracker.GetStatus(ref)
|
||||
if err == nil {
|
||||
if status.Committed && status.Offset == status.Total {
|
||||
@@ -103,6 +99,12 @@ func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, ref str
|
||||
host = hosts[0]
|
||||
)
|
||||
|
||||
base := p.withRewritesFromHost(host)
|
||||
ctx, err = ContextWithRepositoryScope(ctx, base.refspec, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if images.IsManifestType(desc.MediaType) || images.IsIndexType(desc.MediaType) {
|
||||
isManifest = true
|
||||
existCheck = getManifestPath(p.object, desc.Digest)
|
||||
@@ -110,7 +112,7 @@ func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, ref str
|
||||
existCheck = []string{"blobs", desc.Digest.String()}
|
||||
}
|
||||
|
||||
req := p.request(host, http.MethodHead, existCheck...)
|
||||
req := base.request(host, http.MethodHead, existCheck...)
|
||||
req.header.Set("Accept", strings.Join([]string{desc.MediaType, `*/*`}, ", "))
|
||||
|
||||
log.G(ctx).WithField("url", req.String()).Debugf("checking and pushing to")
|
||||
@@ -158,11 +160,11 @@ func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, ref str
|
||||
|
||||
if isManifest {
|
||||
putPath := getManifestPath(p.object, desc.Digest)
|
||||
req = p.request(host, http.MethodPut, putPath...)
|
||||
req = base.request(host, http.MethodPut, putPath...)
|
||||
req.header.Add("Content-Type", desc.MediaType)
|
||||
} else {
|
||||
// Start upload request
|
||||
req = p.request(host, http.MethodPost, "blobs", "uploads/")
|
||||
req = base.request(host, http.MethodPost, "blobs", "uploads/")
|
||||
|
||||
mountedFrom := ""
|
||||
var resp *http.Response
|
||||
|
||||
@@ -74,6 +74,7 @@ type RegistryHost struct {
|
||||
Path string
|
||||
Capabilities HostCapabilities
|
||||
Header http.Header
|
||||
Rewrites map[string]string
|
||||
}
|
||||
|
||||
func (h RegistryHost) isProxy(refhost string) bool {
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -239,14 +240,13 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
||||
if err != nil {
|
||||
return "", ocispec.Descriptor{}, err
|
||||
}
|
||||
refspec := base.refspec
|
||||
if refspec.Object == "" {
|
||||
if base.refspec.Object == "" {
|
||||
return "", ocispec.Descriptor{}, reference.ErrObjectRequired
|
||||
}
|
||||
|
||||
var (
|
||||
paths [][]string
|
||||
dgst = refspec.Digest()
|
||||
dgst = base.refspec.Digest()
|
||||
caps = HostCapabilityPull
|
||||
)
|
||||
|
||||
@@ -264,7 +264,7 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
||||
paths = append(paths, []string{"blobs", dgst.String()})
|
||||
} else {
|
||||
// Add
|
||||
paths = append(paths, []string{"manifests", refspec.Object})
|
||||
paths = append(paths, []string{"manifests", base.refspec.Object})
|
||||
caps |= HostCapabilityResolve
|
||||
}
|
||||
|
||||
@@ -273,11 +273,6 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
||||
return "", ocispec.Descriptor{}, fmt.Errorf("no resolve hosts: %w", errdefs.ErrNotFound)
|
||||
}
|
||||
|
||||
ctx, err = ContextWithRepositoryScope(ctx, refspec, false)
|
||||
if err != nil {
|
||||
return "", ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
var (
|
||||
// firstErr is the most relevant error encountered during resolution.
|
||||
// We use this to determine the error to return, making sure that the
|
||||
@@ -296,7 +291,11 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
||||
for _, u := range paths {
|
||||
for i, host := range hosts {
|
||||
ctx := log.WithLogger(ctx, log.G(ctx).WithField("host", host.Host))
|
||||
|
||||
base := base.withRewritesFromHost(host)
|
||||
ctx, err = ContextWithRepositoryScope(ctx, base.refspec, false)
|
||||
if err != nil {
|
||||
return "", ocispec.Descriptor{}, err
|
||||
}
|
||||
req := base.request(host, http.MethodHead, u...)
|
||||
if err := req.addNamespace(base.refspec.Hostname()); err != nil {
|
||||
return "", ocispec.Descriptor{}, err
|
||||
@@ -501,7 +500,6 @@ func (r *dockerBase) request(host RegistryHost, method string, ps ...string) *re
|
||||
if header == nil {
|
||||
header = http.Header{}
|
||||
}
|
||||
|
||||
for key, value := range host.Header {
|
||||
header[key] = append(header[key], value...)
|
||||
}
|
||||
@@ -519,6 +517,28 @@ func (r *dockerBase) request(host RegistryHost, method string, ps ...string) *re
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dockerBase) withRewritesFromHost(host RegistryHost) *dockerBase {
|
||||
for pattern, replace := range host.Rewrites {
|
||||
exp, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
log.L.WithError(err).Warnf("Failed to compile rewrite, `%s`, for %s", pattern, host.Host)
|
||||
continue
|
||||
}
|
||||
if rr := exp.ReplaceAllString(r.repository, replace); rr != r.repository {
|
||||
log.L.Debugf("Rewrote repository for %s: %s => %s", r.refspec, r.repository, rr)
|
||||
return &dockerBase{
|
||||
refspec: reference.Spec{
|
||||
Locator: r.refspec.Hostname() + "/" + rr,
|
||||
Object: r.refspec.Object,
|
||||
},
|
||||
repository: rr,
|
||||
header: r.header,
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *request) authorize(ctx context.Context, req *http.Request) error {
|
||||
// Check if has header for host
|
||||
if r.host.Authorizer != nil {
|
||||
|
||||
Reference in New Issue
Block a user