Skip to content

Commit 506b815

Browse files
committed
remotes: add distribution labels to blob data
We can use cross repository push feature to reuse the existing blobs in the same registry. Before make push fast, we know where the blob comes from. Use the `containerd.io/distribution.source. = [,]` as label format. For example, the blob is downloaded by the docker.io/library/busybox:latest and the label will be containerd.io/distribution.source.docker.io = library/busybox If the blob is shared by different repos in the same registry, the repo name will be appended, like: containerd.io/distribution.source.docker.io = library/busybox,x/y NOTE: 1. no need to apply for legacy docker image schema1. 2. the concurrent fetch actions might miss some repo names in label, but it is ok. 3. it is optional. no need to add label if the engine only uses images not push. Signed-off-by: Wei Fu <[email protected]>
1 parent e70a530 commit 506b815

9 files changed

Lines changed: 315 additions & 6 deletions

File tree

client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ type RemoteContext struct {
300300

301301
// MaxConcurrentDownloads is the max concurrent content downloads for each pull.
302302
MaxConcurrentDownloads int
303+
304+
// AppendDistributionSourceLabel allows fetcher to add distribute source
305+
// label for each blob content, which doesn't work for legacy schema1.
306+
AppendDistributionSourceLabel bool
303307
}
304308

305309
func defaultRemoteContext() *RemoteContext {

client_opts.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,12 @@ func WithMaxConcurrentDownloads(max int) RemoteOpt {
194194
return nil
195195
}
196196
}
197+
198+
// WithAppendDistributionSourceLabel allows fetcher to add distribute source
199+
// label for each blob content, which doesn't work for legacy schema1.
200+
func WithAppendDistributionSourceLabel() RemoteOpt {
201+
return func(_ *Client, c *RemoteContext) error {
202+
c.AppendDistributionSourceLabel = true
203+
return nil
204+
}
205+
}

client_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ func TestImagePullSomePlatforms(t *testing.T) {
281281
count := 0
282282
for _, manifest := range manifests {
283283
children, err := images.Children(ctx, cs, manifest)
284+
if err != nil {
285+
t.Fatal(err)
286+
}
287+
284288
found := false
285289
for _, matcher := range m {
286290
if matcher.Match(*manifest.Platform) {
@@ -302,8 +306,6 @@ func TestImagePullSomePlatforms(t *testing.T) {
302306
}
303307
ra.Close()
304308
}
305-
} else if !found && err == nil {
306-
t.Fatal("manifest should not have pulled children content")
307309
}
308310
}
309311

cmd/ctr/commands/content/fetch.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,13 @@ func Fetch(ctx context.Context, client *containerd.Client, ref string, config *F
149149
containerd.WithResolver(config.Resolver),
150150
containerd.WithImageHandler(h),
151151
containerd.WithSchema1Conversion,
152+
containerd.WithAppendDistributionSourceLabel(),
152153
}
154+
153155
for _, platform := range config.Platforms {
154156
opts = append(opts, containerd.WithPlatform(platform))
155157
}
158+
156159
img, err := client.Fetch(pctx, ref, opts...)
157160
stopProgress()
158161
if err != nil {

image_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@
1717
package containerd
1818

1919
import (
20+
"context"
21+
"fmt"
2022
"runtime"
23+
"strings"
2124
"testing"
2225

2326
"github.com/containerd/containerd/errdefs"
27+
"github.com/containerd/containerd/images"
28+
"github.com/containerd/containerd/platforms"
29+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2430
)
2531

2632
func TestImageIsUnpacked(t *testing.T) {
@@ -72,3 +78,60 @@ func TestImageIsUnpacked(t *testing.T) {
7278
t.Fatalf("image should be unpacked")
7379
}
7480
}
81+
82+
func TestImagePullWithDistSourceLabel(t *testing.T) {
83+
var (
84+
source = "docker.io"
85+
repoName = "library/busybox"
86+
tag = "latest"
87+
)
88+
89+
ctx, cancel := testContext()
90+
defer cancel()
91+
92+
client, err := newClient(t, address)
93+
if err != nil {
94+
t.Fatal(err)
95+
}
96+
defer client.Close()
97+
98+
imageName := fmt.Sprintf("%s/%s:%s", source, repoName, tag)
99+
pMatcher := platforms.Default()
100+
101+
// pull content without unpack and add distribution source label
102+
image, err := client.Pull(ctx, imageName,
103+
WithPlatformMatcher(pMatcher),
104+
WithAppendDistributionSourceLabel())
105+
if err != nil {
106+
t.Fatal(err)
107+
}
108+
defer client.ImageService().Delete(ctx, imageName)
109+
110+
cs := client.ContentStore()
111+
key := fmt.Sprintf("containerd.io/distribution.source.%s", source)
112+
113+
// only check the target platform
114+
childrenHandler := images.FilterPlatforms(images.ChildrenHandler(cs), pMatcher)
115+
116+
checkLabelHandler := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
117+
children, err := childrenHandler(ctx, desc)
118+
if err != nil {
119+
return nil, err
120+
}
121+
122+
info, err := cs.Info(ctx, desc.Digest)
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
// check the label
128+
if got := info.Labels[key]; !strings.Contains(got, repoName) {
129+
return nil, fmt.Errorf("expected to have %s repo name in label, but got %s", repoName, got)
130+
}
131+
return children, nil
132+
}
133+
134+
if err := images.Dispatch(ctx, images.HandlerFunc(checkLabelHandler), nil, image.Target()); err != nil {
135+
t.Fatal(err)
136+
}
137+
}

pull.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ func (c *Client) fetch(ctx context.Context, rCtx *RemoteContext, ref string, lim
112112
childrenHandler := images.ChildrenHandler(store)
113113
// Set any children labels for that content
114114
childrenHandler = images.SetChildrenLabels(store, childrenHandler)
115-
// Filter children by platforms
116-
childrenHandler = images.FilterPlatforms(childrenHandler, rCtx.PlatformMatcher)
115+
// Filter manifests by platforms but allow to handle manifest
116+
// and configuration for not-target platforms
117+
childrenHandler = remotes.FilterManifestByPlatformHandler(childrenHandler, rCtx.PlatformMatcher)
117118
// Sort and limit manifests if a finite number is needed
118119
if limit > 0 {
119120
childrenHandler = images.LimitManifests(childrenHandler, rCtx.PlatformMatcher, limit)
@@ -130,11 +131,23 @@ func (c *Client) fetch(ctx context.Context, rCtx *RemoteContext, ref string, lim
130131
},
131132
)
132133

133-
handler = images.Handlers(append(rCtx.BaseHandlers,
134+
handlers := append(rCtx.BaseHandlers,
134135
remotes.FetchHandler(store, fetcher),
135136
convertibleHandler,
136137
childrenHandler,
137-
)...)
138+
)
139+
140+
// append distribution source label to blob data
141+
if rCtx.AppendDistributionSourceLabel {
142+
appendDistSrcLabelHandler, err := docker.AppendDistributionSourceLabel(store, ref)
143+
if err != nil {
144+
return images.Image{}, err
145+
}
146+
147+
handlers = append(handlers, appendDistSrcLabelHandler)
148+
}
149+
150+
handler = images.Handlers(handlers...)
138151

139152
converterFunc = func(ctx context.Context, desc ocispec.Descriptor) (ocispec.Descriptor, error) {
140153
return docker.ConvertManifest(ctx, store, desc)
@@ -148,6 +161,7 @@ func (c *Client) fetch(ctx context.Context, rCtx *RemoteContext, ref string, lim
148161
if rCtx.MaxConcurrentDownloads > 0 {
149162
limiter = semaphore.NewWeighted(int64(rCtx.MaxConcurrentDownloads))
150163
}
164+
151165
if err := images.Dispatch(ctx, handler, limiter, desc); err != nil {
152166
return images.Image{}, err
153167
}

remotes/docker/handler.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package docker
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"net/url"
23+
"strings"
24+
25+
"github.com/containerd/containerd/content"
26+
"github.com/containerd/containerd/images"
27+
"github.com/containerd/containerd/labels"
28+
"github.com/containerd/containerd/log"
29+
"github.com/containerd/containerd/reference"
30+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
31+
)
32+
33+
var (
34+
// labelDistributionSource describes the source blob comes from.
35+
labelDistributionSource = "containerd.io/distribution.source"
36+
)
37+
38+
// AppendDistributionSourceLabel updates the label of blob with distribution source.
39+
func AppendDistributionSourceLabel(manager content.Manager, ref string) (images.HandlerFunc, error) {
40+
refspec, err := reference.Parse(ref)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
u, err := url.Parse("dummy://" + refspec.Locator)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/")
51+
return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
52+
info, err := manager.Info(ctx, desc.Digest)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
key := distributionSourceLabelKey(source)
58+
59+
originLabel := ""
60+
if info.Labels != nil {
61+
originLabel = info.Labels[key]
62+
}
63+
value := appendDistributionSourceLabel(originLabel, repo)
64+
65+
// The repo name has been limited under 256 and the distribution
66+
// label might hit the limitation of label size, when blob data
67+
// is used as the very, very common layer.
68+
if err := labels.Validate(key, value); err != nil {
69+
log.G(ctx).Warnf("skip to append distribution label: %s", err)
70+
return nil, nil
71+
}
72+
73+
info = content.Info{
74+
Digest: desc.Digest,
75+
Labels: map[string]string{
76+
key: value,
77+
},
78+
}
79+
_, err = manager.Update(ctx, info, fmt.Sprintf("labels.%s", key))
80+
return nil, err
81+
}, nil
82+
}
83+
84+
func appendDistributionSourceLabel(originLabel, repo string) string {
85+
repos := []string{}
86+
if originLabel != "" {
87+
repos = strings.Split(originLabel, ",")
88+
}
89+
repos = append(repos, repo)
90+
91+
// use emtpy string to present duplicate items
92+
for i := 1; i < len(repos); i++ {
93+
tmp, j := repos[i], i-1
94+
for ; j >= 0 && repos[j] >= tmp; j-- {
95+
if repos[j] == tmp {
96+
tmp = ""
97+
}
98+
repos[j+1] = repos[j]
99+
}
100+
repos[j+1] = tmp
101+
}
102+
103+
i := 0
104+
for ; i < len(repos) && repos[i] == ""; i++ {
105+
}
106+
107+
return strings.Join(repos[i:], ",")
108+
}
109+
110+
func distributionSourceLabelKey(source string) string {
111+
return fmt.Sprintf("%s.%s", labelDistributionSource, source)
112+
}

remotes/docker/handler_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package docker
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
)
23+
24+
func TestAppendDistributionLabel(t *testing.T) {
25+
for _, tc := range []struct {
26+
originLabel string
27+
repo string
28+
expected string
29+
}{
30+
{
31+
originLabel: "",
32+
repo: "",
33+
expected: "",
34+
},
35+
{
36+
originLabel: "",
37+
repo: "library/busybox",
38+
expected: "library/busybox",
39+
},
40+
{
41+
originLabel: "library/busybox",
42+
repo: "library/busybox",
43+
expected: "library/busybox",
44+
},
45+
// remove the duplicate one in origin
46+
{
47+
originLabel: "library/busybox,library/redis,library/busybox",
48+
repo: "library/alpine",
49+
expected: "library/alpine,library/busybox,library/redis",
50+
},
51+
// remove the empty repo
52+
{
53+
originLabel: "library/busybox,library/redis,library/busybox",
54+
repo: "",
55+
expected: "library/busybox,library/redis",
56+
},
57+
{
58+
originLabel: "library/busybox,library/redis,library/busybox",
59+
repo: "library/redis",
60+
expected: "library/busybox,library/redis",
61+
},
62+
} {
63+
if got := appendDistributionSourceLabel(tc.originLabel, tc.repo); !reflect.DeepEqual(got, tc.expected) {
64+
t.Fatalf("expected %v, but got %v", tc.expected, got)
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)