Skip to content

Commit 11e7716

Browse files
committed
Fix convertor output tagging on manifest dedup
Deduplicated manifests now account for possible adjustments to output tag. Updated tests and test resources to account for this scenario. Signed-off-by: Esteban <esrey@microsoft.com>
1 parent 0c2f057 commit 11e7716

File tree

10 files changed

+97
-15
lines changed

10 files changed

+97
-15
lines changed

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)