Skip to content

Commit

Permalink
Merge pull request #312 from estebanreyl/esrey/bugfix-tags-on-manifes…
Browse files Browse the repository at this point in the history
…t-deduplication

Fix userspace convertor output tagging on manifest dedup
  • Loading branch information
BigVan authored Nov 13, 2024
2 parents 0be64d1 + 9d992a4 commit 657a32f
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 15 deletions.
8 changes: 7 additions & 1 deletion cmd/convertor/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,13 @@ func (b *overlaybdBuilder) Build(ctx context.Context) (v1.Descriptor, error) {
// when errors are encountered fallback to regular conversion
if convertedDesc, err := b.engine.CheckForConvertedManifest(ctx); err == nil && convertedDesc.Digest != "" {
logrus.Infof("Image found already converted in registry with digest %s", convertedDesc.Digest)
return convertedDesc, nil
// Even if the image has been found we still need to make sure the requested tag is set
// fetch the manifest then push again with the requested tag
if err := b.engine.TagPreviouslyConvertedManifest(ctx, convertedDesc); err != nil {
logrus.Warnf("failed to tag previously converted manifest: %s. Falling back to regular conversion", err)
} else {
return convertedDesc, nil
}
}

// Errgroups will close the context after wait returns so the operations need their own
Expand Down
3 changes: 3 additions & 0 deletions cmd/convertor/builder/builder_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ type Deduplicateable interface {
// store manifest digest -> converted manifest to avoid re-conversion
CheckForConvertedManifest(ctx context.Context) (specs.Descriptor, error)

// tag a converted manifest -> converted manifest to avoid re-conversion
TagPreviouslyConvertedManifest(ctx context.Context, desc specs.Descriptor) error

// store manifest digest -> converted manifest to avoid re-conversion
StoreConvertedManifestDetails(ctx context.Context) error
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/convertor/builder/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,9 @@ func (e *mockFuzzBuilderEngine) DownloadConvertedLayer(ctx context.Context, idx
return nil
}

func (e *mockFuzzBuilderEngine) TagPreviouslyConvertedManifest(ctx context.Context, desc specs.Descriptor) error {
return nil
}

func (e *mockFuzzBuilderEngine) Cleanup() {
}
15 changes: 15 additions & 0 deletions cmd/convertor/builder/builder_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,21 @@ func uploadBytes(ctx context.Context, pusher remotes.Pusher, desc specs.Descript
return content.Copy(ctx, cw, bytes.NewReader(data), desc.Size, desc.Digest)
}

func tagPreviouslyConvertedManifest(ctx context.Context, pusher remotes.Pusher, fetcher remotes.Fetcher, desc specs.Descriptor) error {
manifest := specs.Manifest{}
if err := fetch(ctx, fetcher, desc, &manifest); err != nil {
return fmt.Errorf("failed to fetch converted manifest: %w", err)
}
cbuf, err := json.Marshal(manifest)
if err != nil {
return err
}
if err := uploadBytes(ctx, pusher, desc, cbuf); err != nil {
return fmt.Errorf("failed to tag converted manifest: %w", err)
}
return nil
}

func buildArchiveFromFiles(ctx context.Context, target string, compress compression.Compression, files ...string) error {
archive, err := os.Create(target)
if err != nil {
Expand Down
19 changes: 19 additions & 0 deletions cmd/convertor/builder/builder_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,22 @@ func Test_writeConfig(t *testing.T) {
}
})
}

func Test_tagPreviouslyConvertedManifest(t *testing.T) {
ctx := context.Background()
resolver := testingresources.GetTestResolver(t, ctx)
pusher := testingresources.GetTestPusherFromResolver(t, ctx, resolver, "sample.localstore.io/hello-world:anothertag")
fetcher := testingresources.GetTestFetcherFromResolver(t, ctx, resolver, testingresources.DockerV2_Manifest_Simple_Converted_Ref)

_, convertedDesc, err := resolver.Resolve(ctx, testingresources.DockerV2_Manifest_Simple_Converted_Ref) // Simulate a previously converted manifest
testingresources.Assert(t, err == nil, "Could not resolve manifest")
convertedDesc.Annotations = map[string]string{} // Simulate a manifest that has been converted and is found by digest

err = tagPreviouslyConvertedManifest(ctx, pusher, fetcher, convertedDesc)
testingresources.Assert(t, err == nil, "Could not tag previously converted manifest")

// Check if the manifest was tagged correctly
_, desc, err := resolver.Resolve(ctx, "sample.localstore.io/hello-world:anothertag")
testingresources.Assert(t, err == nil, "Could not resolve tagged manifest")
testingresources.Assert(t, desc.Digest == convertedDesc.Digest, "Tagged manifest digest does not match original")
}
5 changes: 5 additions & 0 deletions cmd/convertor/builder/overlaybd_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,11 @@ func (e *overlaybdBuilderEngine) CheckForConvertedManifest(ctx context.Context)
return specs.Descriptor{}, errdefs.ErrNotFound
}

// If a converted manifest has been found we still need to tag it to match the expected output tag.
func (e *overlaybdBuilderEngine) TagPreviouslyConvertedManifest(ctx context.Context, desc specs.Descriptor) error {
return tagPreviouslyConvertedManifest(ctx, e.pusher, e.fetcher, desc)
}

// mountImage is responsible for mounting a specific manifest from a source repository, this includes
// mounting all layers + config and then pushing the manifest.
func (e *overlaybdBuilderEngine) mountImage(ctx context.Context, manifest specs.Manifest, desc specs.Descriptor, mountRepository string) error {
Expand Down
5 changes: 5 additions & 0 deletions cmd/convertor/builder/turboOCI_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ func (e *turboOCIBuilderEngine) UploadImage(ctx context.Context) (specs.Descript
return e.uploadManifestAndConfig(ctx)
}

// If a converted manifest has been found we still need to tag it to match the expected output tag.
func (e *turboOCIBuilderEngine) TagPreviouslyConvertedManifest(ctx context.Context, desc specs.Descriptor) error {
return tagPreviouslyConvertedManifest(ctx, e.pusher, e.fetcher, desc)
}

// Layer deduplication in FastOCI is not currently supported due to conversion not
// being reproducible at the moment which can lead to occasional bugs.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@ mysqldbpassword=$8 # mysql password
oras login $registry -u $username -p $password
oras cp $sourceImage $registry/$repository:$tag
# Try one conversion
./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
./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

# Retry, result manifest should be cached
./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
./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

# Retry, cross repo mount
oras cp $sourceImage $registry/$repository-2:$tag
./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
./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

# Expected output in the registry:
# <Repository>
# -- <Tag>
# -- <Tag>-obd-cache
# -- <Tag>-obd-cache-2
# <Repository>-2
# -- <Tag>
# -- <Tag>-obd-cache-3
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ RUN apt update && \
apt install -y libcurl4-openssl-dev libext2fs-dev libaio-dev mysql-server

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

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

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

# EXTRAS
# Useful resources
COPY cmd/convertor/resources/samples/mysql.conf ./mysql.conf
COPY cmd/convertor/resources/samples/mysql-db-setup.sh ./mysql-db-setup.sh
COPY cmd/convertor/resources/samples/mysql-db-manifest-cache-sample-workload.sh ./mysql-db-manifest-cache-sample-workload.sh

RUN apt update && apt install -y wget
# Add Oras CLI
RUN wget "https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz" && \
mkdir -p oras-install/ && \
tar -zxf oras_1.2.0_*.tar.gz -C oras-install/ && \
mv oras-install/oras /usr/local/bin/ && \
rm -rf oras_1.2.0_*.tar.gz oras-install/

CMD ["./bin/convertor"]
20 changes: 13 additions & 7 deletions cmd/convertor/testingresources/local_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,18 +163,22 @@ func (r *RepoStore) Exists(ctx context.Context, descriptor v1.Descriptor) (bool,
// Push pushes a blob to the in memory repository. If the blob already exists, it returns an error.
// Tag is optional and can be empty.
func (r *RepoStore) Push(ctx context.Context, desc v1.Descriptor, tag string, content []byte) error {
exists, err := r.Exists(ctx, desc)
manifestExists, err := r.Exists(ctx, desc)
if err != nil {
return err
}
if exists {
return nil // No error is returned on push if image is already present

// Verify content by computing the digest. Real registries are expected to do this.
contentDigest := digest.FromBytes(content)
if contentDigest != desc.Digest {
return fmt.Errorf("content digest %s does not match descriptor digest %s", contentDigest.String(), desc.Digest.String())
}

isManifest := false
switch desc.MediaType {
case v1.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest:
isManifest = true
if r.opts.ManifestPushIgnoresLayers {
if r.opts.ManifestPushIgnoresLayers || manifestExists {
break // No layer verification necessary
}
manifest := v1.Manifest{}
Expand All @@ -185,12 +189,12 @@ func (r *RepoStore) Push(ctx context.Context, desc v1.Descriptor, tag string, co
return err
}
if !exists {
return fmt.Errorf("Layer %s not found", layer.Digest.String())
return fmt.Errorf("layer %s not found", layer.Digest.String())
}
}
case v1.MediaTypeImageIndex, images.MediaTypeDockerSchema2ManifestList:
isManifest = true
if r.opts.ManifestPushIgnoresLayers {
if r.opts.ManifestPushIgnoresLayers || manifestExists {
break // No manifest verification necessary
}
manifestList := v1.Index{}
Expand All @@ -201,12 +205,14 @@ func (r *RepoStore) Push(ctx context.Context, desc v1.Descriptor, tag string, co
return err
}
if !exists {
return fmt.Errorf("Sub manifest %s not found", subManifestDesc.Digest.String())
return fmt.Errorf("sub manifest %s not found", subManifestDesc.Digest.String())
}
}
}
r.inmemoryRepo.blobs[desc.Digest.String()] = content

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

0 comments on commit 657a32f

Please sign in to comment.