Skip to content

Commit 67bb218

Browse files
committed
Allow optimizing images with prioritiezed files info shared via registry
Signed-off-by: Kohei Tokunaga <ktokunaga.mail@gmail.com>
1 parent cf57861 commit 67bb218

File tree

13 files changed

+925
-84
lines changed

13 files changed

+925
-84
lines changed

analyzer/analyzer.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func Analyze(ctx context.Context, client *containerd.Client, ref string, opts ..
188188
})
189189

190190
// Start to monitor "/" and run the task.
191-
rc, err := recorder.NewImageRecorder(ctx, cs, img, platforms.Default())
191+
rc, err := recorder.NewImageRecorder(ctx, cs, img, platforms.DefaultStrict())
192192
if err != nil {
193193
return "", err
194194
}

analyzer/recorder/images.go

+253
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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 recorder
18+
19+
import (
20+
"archive/tar"
21+
"compress/gzip"
22+
"context"
23+
"encoding/json"
24+
"fmt"
25+
"io"
26+
27+
"github.com/containerd/containerd"
28+
"github.com/containerd/containerd/archive/compression"
29+
"github.com/containerd/containerd/content"
30+
"github.com/containerd/containerd/errdefs"
31+
"github.com/containerd/containerd/images"
32+
"github.com/containerd/containerd/labels"
33+
"github.com/containerd/containerd/platforms"
34+
"github.com/containerd/stargz-snapshotter/util/containerdutil"
35+
"github.com/opencontainers/go-digest"
36+
ocispecVersion "github.com/opencontainers/image-spec/specs-go"
37+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
38+
)
39+
40+
const recordJSON = "stargz.record.json"
41+
42+
// RecordOutToImage writes the specified record out blob as an image.
43+
func RecordOutToImage(ctx context.Context, client *containerd.Client, recordOutDgst digest.Digest, ref string) (*images.Image, error) {
44+
cs := client.ContentStore()
45+
is := client.ImageService()
46+
47+
// Write blob
48+
ra, err := cs.ReaderAt(ctx, ocispec.Descriptor{Digest: recordOutDgst})
49+
if err != nil {
50+
return nil, err
51+
}
52+
defer ra.Close()
53+
recordSize := ra.Size()
54+
sr := io.NewSectionReader(ra, 0, recordSize)
55+
blobW, err := content.OpenWriter(ctx, cs, content.WithRef(fmt.Sprintf("recording-ref-%s", recordOutDgst)))
56+
if err != nil {
57+
return nil, err
58+
}
59+
defer blobW.Close()
60+
if err := blobW.Truncate(0); err != nil {
61+
return nil, err
62+
}
63+
zw := gzip.NewWriter(blobW)
64+
defer zw.Close()
65+
diffID := digest.Canonical.Digester()
66+
tw := tar.NewWriter(io.MultiWriter(zw, diffID.Hash()))
67+
if err := tw.WriteHeader(&tar.Header{
68+
Name: recordJSON,
69+
Typeflag: tar.TypeReg,
70+
Size: recordSize,
71+
}); err != nil {
72+
return nil, err
73+
}
74+
if _, err := io.CopyN(tw, sr, recordSize); err != nil {
75+
return nil, err
76+
}
77+
if err := tw.Close(); err != nil {
78+
return nil, err
79+
}
80+
if err := zw.Close(); err != nil {
81+
return nil, err
82+
}
83+
blobLabels := map[string]string{
84+
labels.LabelUncompressed: diffID.Digest().String(),
85+
}
86+
if err := blobW.Commit(ctx, 0, "", content.WithLabels(blobLabels)); err != nil && !errdefs.IsAlreadyExists(err) {
87+
return nil, err
88+
}
89+
blobInfo, err := cs.Info(ctx, blobW.Digest())
90+
if err != nil {
91+
return nil, err
92+
}
93+
blobDesc := ocispec.Descriptor{
94+
MediaType: ocispec.MediaTypeImageLayerGzip,
95+
Digest: blobInfo.Digest,
96+
Size: blobInfo.Size,
97+
}
98+
if err := blobW.Close(); err != nil {
99+
return nil, err
100+
}
101+
102+
// Write config
103+
configW, err := content.OpenWriter(ctx, cs, content.WithRef(fmt.Sprintf("recording-ref-config-%s", recordOutDgst)))
104+
if err != nil {
105+
return nil, err
106+
}
107+
defer configW.Close()
108+
if err := json.NewEncoder(configW).Encode(ocispec.Image{
109+
Architecture: platforms.DefaultSpec().Architecture,
110+
OS: platforms.DefaultSpec().OS,
111+
RootFS: ocispec.RootFS{
112+
Type: "layers",
113+
DiffIDs: []digest.Digest{diffID.Digest()},
114+
},
115+
}); err != nil {
116+
return nil, err
117+
}
118+
if err := configW.Commit(ctx, 0, ""); err != nil && !errdefs.IsAlreadyExists(err) {
119+
return nil, err
120+
}
121+
configInfo, err := cs.Info(ctx, configW.Digest())
122+
if err != nil {
123+
return nil, err
124+
}
125+
configDesc := ocispec.Descriptor{
126+
MediaType: ocispec.MediaTypeImageConfig,
127+
Digest: configInfo.Digest,
128+
Size: configInfo.Size,
129+
}
130+
if err := configW.Close(); err != nil {
131+
return nil, err
132+
}
133+
134+
// Write manifest
135+
manifestW, err := content.OpenWriter(ctx, cs, content.WithRef(fmt.Sprintf("recording-ref-manifest-%s", recordOutDgst)))
136+
if err != nil {
137+
return nil, err
138+
}
139+
defer manifestW.Close()
140+
if err := json.NewEncoder(manifestW).Encode(ocispec.Manifest{
141+
Versioned: ocispecVersion.Versioned{
142+
SchemaVersion: 2,
143+
},
144+
MediaType: ocispec.MediaTypeImageManifest,
145+
Config: configDesc,
146+
Layers: []ocispec.Descriptor{blobDesc},
147+
}); err != nil {
148+
return nil, err
149+
}
150+
if err := manifestW.Commit(ctx, 0, "", content.WithLabels(map[string]string{
151+
"containerd.io/gc.ref.content.record.config": configDesc.Digest.String(),
152+
"containerd.io/gc.ref.content.record.blob": blobDesc.Digest.String(),
153+
})); err != nil && !errdefs.IsAlreadyExists(err) {
154+
return nil, err
155+
}
156+
manifestInfo, err := cs.Info(ctx, manifestW.Digest())
157+
if err != nil {
158+
return nil, err
159+
}
160+
manifestDesc := ocispec.Descriptor{
161+
MediaType: ocispec.MediaTypeImageManifest,
162+
Digest: manifestInfo.Digest,
163+
Size: manifestInfo.Size,
164+
}
165+
if err := manifestW.Close(); err != nil {
166+
return nil, err
167+
}
168+
169+
// Write image
170+
_ = is.Delete(ctx, ref)
171+
res, err := is.Create(ctx, images.Image{
172+
Name: ref,
173+
Target: manifestDesc,
174+
})
175+
return &res, err
176+
}
177+
178+
// RecordInFromImage gets a record out file from the specified image.
179+
func RecordInFromImage(ctx context.Context, client *containerd.Client, ref string, platform platforms.MatchComparer) (digest.Digest, error) {
180+
is := client.ImageService()
181+
cs := client.ContentStore()
182+
183+
i, err := is.Get(ctx, ref)
184+
if err != nil {
185+
return "", err
186+
}
187+
188+
manifestDesc, err := containerdutil.ManifestDesc(ctx, cs, i.Target, platform)
189+
if err != nil {
190+
return "", err
191+
}
192+
p, err := content.ReadBlob(ctx, cs, manifestDesc)
193+
if err != nil {
194+
return "", err
195+
}
196+
var manifest ocispec.Manifest
197+
if err := json.Unmarshal(p, &manifest); err != nil {
198+
return "", err
199+
}
200+
if len(manifest.Layers) != 1 {
201+
return "", fmt.Errorf("record image must have 1 layer")
202+
}
203+
recordOut := manifest.Layers[0]
204+
205+
ra, err := cs.ReaderAt(ctx, recordOut)
206+
if err != nil {
207+
return "", err
208+
}
209+
defer ra.Close()
210+
dr, err := compression.DecompressStream(io.NewSectionReader(ra, 0, ra.Size()))
211+
if err != nil {
212+
return "", err
213+
}
214+
var recordOutR io.Reader
215+
var recordOutSize int64
216+
tr := tar.NewReader(dr)
217+
for {
218+
h, err := tr.Next()
219+
if err != nil {
220+
if err == io.EOF {
221+
break
222+
} else {
223+
return "", err
224+
}
225+
}
226+
if cleanEntryName(h.Name) == recordJSON {
227+
recordOutR, recordOutSize = tr, h.Size
228+
break
229+
}
230+
}
231+
if recordOutR == nil {
232+
return "", fmt.Errorf("failed to find record file")
233+
}
234+
recordW, err := content.OpenWriter(ctx, cs, content.WithRef(fmt.Sprintf("recording-in-ref-%s", manifestDesc.Digest)))
235+
if err != nil {
236+
return "", err
237+
}
238+
defer recordW.Close()
239+
if err := recordW.Truncate(0); err != nil {
240+
return "", err
241+
}
242+
if _, err := io.CopyN(recordW, recordOutR, recordOutSize); err != nil {
243+
return "", err
244+
}
245+
if err := recordW.Commit(ctx, 0, ""); err != nil && !errdefs.IsAlreadyExists(err) {
246+
return "", err
247+
}
248+
dgst := recordW.Digest()
249+
if err := recordW.Close(); err != nil {
250+
return "", err
251+
}
252+
return dgst, nil
253+
}

analyzer/recorder/recorder.go

-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import (
3131
"github.com/containerd/containerd/errdefs"
3232
"github.com/containerd/containerd/images"
3333
"github.com/containerd/containerd/images/converter/uncompress"
34-
"github.com/containerd/containerd/log"
3534
"github.com/containerd/containerd/platforms"
3635
"github.com/containerd/stargz-snapshotter/recorder"
3736
"github.com/containerd/stargz-snapshotter/util/containerdutil"
@@ -84,7 +83,6 @@ func imageRecorderFromManifest(ctx context.Context, cs content.Store, manifestDe
8483
// TODO: During optimization, we uncompress the blob several times (here and during
8584
// creating eStargz layer). We should unify this process for better optimization
8685
// performance.
87-
log.G(ctx).Infof("analyzing blob %q", desc.Digest)
8886
readerAt, err := cs.ReaderAt(ctx, desc)
8987
if err != nil {
9088
return nil, fmt.Errorf("failed to get reader blob %v: %w", desc.Digest, err)

0 commit comments

Comments
 (0)