Skip to content

Commit 657a32f

Browse files
authored
Merge pull request #312 from estebanreyl/esrey/bugfix-tags-on-manifest-deduplication
Fix userspace convertor output tagging on manifest dedup
2 parents 0be64d1 + 9d992a4 commit 657a32f

10 files changed

Lines changed: 97 additions & 15 deletions

cmd/convertor/builder/builder.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,13 @@ func (b *overlaybdBuilder) Build(ctx context.Context) (v1.Descriptor, error) {
363363
// when errors are encountered fallback to regular conversion
364364
if convertedDesc, err := b.engine.CheckForConvertedManifest(ctx); err == nil && convertedDesc.Digest != "" {
365365
logrus.Infof("Image found already converted in registry with digest %s", convertedDesc.Digest)
366-
return convertedDesc, nil
366+
// Even if the image has been found we still need to make sure the requested tag is set
367+
// fetch the manifest then push again with the requested tag
368+
if err := b.engine.TagPreviouslyConvertedManifest(ctx, convertedDesc); err != nil {
369+
logrus.Warnf("failed to tag previously converted manifest: %s. Falling back to regular conversion", err)
370+
} else {
371+
return convertedDesc, nil
372+
}
367373
}
368374

369375
// Errgroups will close the context after wait returns so the operations need their own

cmd/convertor/builder/builder_engine.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ type Deduplicateable interface {
8989
// store manifest digest -> converted manifest to avoid re-conversion
9090
CheckForConvertedManifest(ctx context.Context) (specs.Descriptor, error)
9191

92+
// tag a converted manifest -> converted manifest to avoid re-conversion
93+
TagPreviouslyConvertedManifest(ctx context.Context, desc specs.Descriptor) error
94+
9295
// store manifest digest -> converted manifest to avoid re-conversion
9396
StoreConvertedManifestDetails(ctx context.Context) error
9497
}

cmd/convertor/builder/builder_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,5 +139,9 @@ func (e *mockFuzzBuilderEngine) DownloadConvertedLayer(ctx context.Context, idx
139139
return nil
140140
}
141141

142+
func (e *mockFuzzBuilderEngine) TagPreviouslyConvertedManifest(ctx context.Context, desc specs.Descriptor) error {
143+
return nil
144+
}
145+
142146
func (e *mockFuzzBuilderEngine) Cleanup() {
143147
}

cmd/convertor/builder/builder_utils.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,21 @@ func uploadBytes(ctx context.Context, pusher remotes.Pusher, desc specs.Descript
234234
return content.Copy(ctx, cw, bytes.NewReader(data), desc.Size, desc.Digest)
235235
}
236236

237+
func tagPreviouslyConvertedManifest(ctx context.Context, pusher remotes.Pusher, fetcher remotes.Fetcher, desc specs.Descriptor) error {
238+
manifest := specs.Manifest{}
239+
if err := fetch(ctx, fetcher, desc, &manifest); err != nil {
240+
return fmt.Errorf("failed to fetch converted manifest: %w", err)
241+
}
242+
cbuf, err := json.Marshal(manifest)
243+
if err != nil {
244+
return err
245+
}
246+
if err := uploadBytes(ctx, pusher, desc, cbuf); err != nil {
247+
return fmt.Errorf("failed to tag converted manifest: %w", err)
248+
}
249+
return nil
250+
}
251+
237252
func buildArchiveFromFiles(ctx context.Context, target string, compress compression.Compression, files ...string) error {
238253
archive, err := os.Create(target)
239254
if err != nil {

cmd/convertor/builder/builder_utils_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,22 @@ func Test_writeConfig(t *testing.T) {
467467
}
468468
})
469469
}
470+
471+
func Test_tagPreviouslyConvertedManifest(t *testing.T) {
472+
ctx := context.Background()
473+
resolver := testingresources.GetTestResolver(t, ctx)
474+
pusher := testingresources.GetTestPusherFromResolver(t, ctx, resolver, "sample.localstore.io/hello-world:anothertag")
475+
fetcher := testingresources.GetTestFetcherFromResolver(t, ctx, resolver, testingresources.DockerV2_Manifest_Simple_Converted_Ref)
476+
477+
_, convertedDesc, err := resolver.Resolve(ctx, testingresources.DockerV2_Manifest_Simple_Converted_Ref) // Simulate a previously converted manifest
478+
testingresources.Assert(t, err == nil, "Could not resolve manifest")
479+
convertedDesc.Annotations = map[string]string{} // Simulate a manifest that has been converted and is found by digest
480+
481+
err = tagPreviouslyConvertedManifest(ctx, pusher, fetcher, convertedDesc)
482+
testingresources.Assert(t, err == nil, "Could not tag previously converted manifest")
483+
484+
// Check if the manifest was tagged correctly
485+
_, desc, err := resolver.Resolve(ctx, "sample.localstore.io/hello-world:anothertag")
486+
testingresources.Assert(t, err == nil, "Could not resolve tagged manifest")
487+
testingresources.Assert(t, desc.Digest == convertedDesc.Digest, "Tagged manifest digest does not match original")
488+
}

cmd/convertor/builder/overlaybd_builder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,11 @@ func (e *overlaybdBuilderEngine) CheckForConvertedManifest(ctx context.Context)
337337
return specs.Descriptor{}, errdefs.ErrNotFound
338338
}
339339

340+
// If a converted manifest has been found we still need to tag it to match the expected output tag.
341+
func (e *overlaybdBuilderEngine) TagPreviouslyConvertedManifest(ctx context.Context, desc specs.Descriptor) error {
342+
return tagPreviouslyConvertedManifest(ctx, e.pusher, e.fetcher, desc)
343+
}
344+
340345
// mountImage is responsible for mounting a specific manifest from a source repository, this includes
341346
// mounting all layers + config and then pushing the manifest.
342347
func (e *overlaybdBuilderEngine) mountImage(ctx context.Context, manifest specs.Manifest, desc specs.Descriptor, mountRepository string) error {

cmd/convertor/builder/turboOCI_builder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ func (e *turboOCIBuilderEngine) UploadImage(ctx context.Context) (specs.Descript
213213
return e.uploadManifestAndConfig(ctx)
214214
}
215215

216+
// If a converted manifest has been found we still need to tag it to match the expected output tag.
217+
func (e *turboOCIBuilderEngine) TagPreviouslyConvertedManifest(ctx context.Context, desc specs.Descriptor) error {
218+
return tagPreviouslyConvertedManifest(ctx, e.pusher, e.fetcher, desc)
219+
}
220+
216221
// Layer deduplication in FastOCI is not currently supported due to conversion not
217222
// being reproducible at the moment which can lead to occasional bugs.
218223

cmd/convertor/resources/samples/mysql-db-manifest-cache-sample-workload.sh

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,20 @@ mysqldbpassword=$8 # mysql password
1111
oras login $registry -u $username -p $password
1212
oras cp $sourceImage $registry/$repository:$tag
1313
# Try one conversion
14-
./bin/convertor --repository $registry/$repository -u $username:$password --input-tag $tag --oci --overlaybd $tag-obd-cache --db-str "$mysqldbuser:mysqldbpassword@tcp(127.0.0.1:3306)/conversioncache" --db-type mysql
14+
./bin/convertor --repository $registry/$repository -u $username:$password --input-tag $tag --oci --overlaybd $tag-obd-cache --db-str "$mysqldbuser:$mysqldbpassword@tcp(127.0.0.1:3306)/conversioncache" --db-type mysql
1515

1616
# Retry, result manifest should be cached
17-
./bin/convertor --repository $registry/$repository -u $username:$password --input-tag $tag --oci --overlaybd $tag-obd-cache-2 --db-str "$mysqldbuser:mysqldbpassword@tcp(127.0.0.1:3306)/conversioncache" --db-type mysql
17+
./bin/convertor --repository $registry/$repository -u $username:$password --input-tag $tag --oci --overlaybd $tag-obd-cache-2 --db-str "$mysqldbuser:$mysqldbpassword@tcp(127.0.0.1:3306)/conversioncache" --db-type mysql
1818

1919
# Retry, cross repo mount
2020
oras cp $sourceImage $registry/$repository-2:$tag
21-
./bin/convertor --repository $registry/$repository -u $username:$password --input-tag $tag --oci --overlaybd $tag-obd-cache-2 --db-str "$mysqldbuser:mysqldbpassword@tcp(127.0.0.1:3306)/conversioncache" --db-type mysql
21+
./bin/convertor --repository $registry/$repository-2 -u $username:$password --input-tag $tag --oci --overlaybd $tag-obd-cache-3 --db-str "$mysqldbuser:$mysqldbpassword@tcp(127.0.0.1:3306)/conversioncache" --db-type mysql
22+
23+
# Expected output in the registry:
24+
# <Repository>
25+
# -- <Tag>
26+
# -- <Tag>-obd-cache
27+
# -- <Tag>-obd-cache-2
28+
# <Repository>-2
29+
# -- <Tag>
30+
# -- <Tag>-obd-cache-3

cmd/convertor/resources/samples/run-userspace-convertor-ubuntu.Dockerfile

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ RUN apt update && \
1111
apt install -y libcurl4-openssl-dev libext2fs-dev libaio-dev mysql-server
1212

1313
# --- OVERLAYBD TOOLS ---
14-
FROM base As overlaybd-build
14+
FROM base AS overlaybd-build
1515
RUN apt update && \
1616
apt install -y libgflags-dev libssl-dev libnl-3-dev libnl-genl-3-dev libzstd-dev && \
1717
apt install -y zlib1g-dev binutils make git wget sudo tar gcc cmake build-essential g++ && \
1818
apt install -y uuid-dev libjson-c-dev libkmod-dev libsystemd-dev autoconf automake libtool libpci-dev nasm && \
1919
apt install -y pkg-config
2020

2121
# Download and install Golang version 1.21
22-
RUN wget https://go.dev/dl/go1.20.12.linux-amd64.tar.gz && \
23-
tar -C /usr/local -xzf go1.20.12.linux-amd64.tar.gz && \
24-
rm go1.20.12.linux-amd64.tar.gz
22+
RUN wget https://go.dev/dl/go1.23.2.linux-amd64.tar.gz && \
23+
tar -C /usr/local -xzf go1.23.2.linux-amd64.tar.gz && \
24+
rm go1.23.2.linux-amd64.tar.gz
2525

2626
# Set environment variables
2727
ENV PATH="/usr/local/go/bin:${PATH}"
@@ -56,8 +56,18 @@ COPY --from=overlaybd-build /opt/overlaybd/baselayers /opt/overlaybd/baselayers
5656
COPY --from=overlaybd-build /etc/overlaybd/overlaybd.json /etc/overlaybd/overlaybd.json
5757
COPY --from=convert-build /home/limiteduser/accelerated-container-image/bin/convertor ./bin/convertor
5858

59+
# EXTRAS
5960
# Useful resources
6061
COPY cmd/convertor/resources/samples/mysql.conf ./mysql.conf
6162
COPY cmd/convertor/resources/samples/mysql-db-setup.sh ./mysql-db-setup.sh
6263
COPY cmd/convertor/resources/samples/mysql-db-manifest-cache-sample-workload.sh ./mysql-db-manifest-cache-sample-workload.sh
64+
65+
RUN apt update && apt install -y wget
66+
# Add Oras CLI
67+
RUN wget "https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz" && \
68+
mkdir -p oras-install/ && \
69+
tar -zxf oras_1.2.0_*.tar.gz -C oras-install/ && \
70+
mv oras-install/oras /usr/local/bin/ && \
71+
rm -rf oras_1.2.0_*.tar.gz oras-install/
72+
6373
CMD ["./bin/convertor"]

cmd/convertor/testingresources/local_repo.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,22 @@ func (r *RepoStore) Exists(ctx context.Context, descriptor v1.Descriptor) (bool,
163163
// Push pushes a blob to the in memory repository. If the blob already exists, it returns an error.
164164
// Tag is optional and can be empty.
165165
func (r *RepoStore) Push(ctx context.Context, desc v1.Descriptor, tag string, content []byte) error {
166-
exists, err := r.Exists(ctx, desc)
166+
manifestExists, err := r.Exists(ctx, desc)
167167
if err != nil {
168168
return err
169169
}
170-
if exists {
171-
return nil // No error is returned on push if image is already present
170+
171+
// Verify content by computing the digest. Real registries are expected to do this.
172+
contentDigest := digest.FromBytes(content)
173+
if contentDigest != desc.Digest {
174+
return fmt.Errorf("content digest %s does not match descriptor digest %s", contentDigest.String(), desc.Digest.String())
172175
}
176+
173177
isManifest := false
174178
switch desc.MediaType {
175179
case v1.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest:
176180
isManifest = true
177-
if r.opts.ManifestPushIgnoresLayers {
181+
if r.opts.ManifestPushIgnoresLayers || manifestExists {
178182
break // No layer verification necessary
179183
}
180184
manifest := v1.Manifest{}
@@ -185,12 +189,12 @@ func (r *RepoStore) Push(ctx context.Context, desc v1.Descriptor, tag string, co
185189
return err
186190
}
187191
if !exists {
188-
return fmt.Errorf("Layer %s not found", layer.Digest.String())
192+
return fmt.Errorf("layer %s not found", layer.Digest.String())
189193
}
190194
}
191195
case v1.MediaTypeImageIndex, images.MediaTypeDockerSchema2ManifestList:
192196
isManifest = true
193-
if r.opts.ManifestPushIgnoresLayers {
197+
if r.opts.ManifestPushIgnoresLayers || manifestExists {
194198
break // No manifest verification necessary
195199
}
196200
manifestList := v1.Index{}
@@ -201,12 +205,14 @@ func (r *RepoStore) Push(ctx context.Context, desc v1.Descriptor, tag string, co
201205
return err
202206
}
203207
if !exists {
204-
return fmt.Errorf("Sub manifest %s not found", subManifestDesc.Digest.String())
208+
return fmt.Errorf("sub manifest %s not found", subManifestDesc.Digest.String())
205209
}
206210
}
207211
}
208212
r.inmemoryRepo.blobs[desc.Digest.String()] = content
209213

214+
// We need to always store the manifest digest to tag mapping as the latest pushed manifest
215+
// may have a different digest than the previous one.
210216
if isManifest && tag != "" {
211217
r.inmemoryRepo.tags[tag] = desc.Digest
212218
}

0 commit comments

Comments
 (0)