Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(import): import single-layer container images directly #548

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions pkg/oci/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

import (
"context"
"os"
"path"
"runtime"
"sync"

"github.com/klauspost/pgzip"
"github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/umoci"
"github.com/opencontainers/umoci/oci/casext"
"github.com/opencontainers/umoci/oci/layer"
"github.com/pkg/errors"
"stackerbuild.io/stacker/pkg/log"
"stackerbuild.io/stacker/pkg/squashfs"
)

func LookupManifest(oci casext.Engine, tag string) (ispec.Manifest, error) {
Expand Down Expand Up @@ -76,3 +86,108 @@

return desc, nil
}

func hasDirEntries(dir string) bool {
ents, err := os.ReadDir(dir)
if err != nil {
return false
}
return len(ents) != 0
}

var tarEx sync.Mutex

// UnpackOne - unpack a single layer (Descriptor) found in ociDir to extractDir
//
// The result of calling unpackOne is either error or the contents available
// at the provided extractDir. The extractDir should be either empty or
// fully populated with this layer.
func UnpackOne(l ispec.Descriptor, ociDir string, extractDir string) error {
// population of a dir is not atomic, at least for tar extraction.
// As a result, we could hasDirEntries(extractDir) at the same time that
// something is un-populating that dir due to a failed extraction (like
// os.RemoveAll below).
// There needs to be a lock on the extract dir (scoped to the overlay storage backend).
// A sync.RWMutex would work well here since it is safe to check as long
// as no one is populating or unpopulating.
if hasDirEntries(extractDir) {
// the directory was already populated.
return nil
}

if squashfs.IsSquashfsMediaType(l.MediaType) {
return squashfs.ExtractSingleSquash(
path.Join(ociDir, "blobs", "sha256", l.Digest.Encoded()), extractDir)
}
switch l.MediaType {
case ispec.MediaTypeImageLayer, ispec.MediaTypeImageLayerGzip:
tarEx.Lock()
defer tarEx.Unlock()

oci, err := umoci.OpenLayout(ociDir)
if err != nil {
return err
}

Check warning on line 130 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L129-L130

Added lines #L129 - L130 were not covered by tests
defer oci.Close()

compressed, err := oci.GetBlob(context.Background(), l.Digest)
if err != nil {
return err
}

Check warning on line 136 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L135-L136

Added lines #L135 - L136 were not covered by tests
defer compressed.Close()

uncompressed, err := pgzip.NewReader(compressed)
if err != nil {
return err
}

Check warning on line 142 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L141-L142

Added lines #L141 - L142 were not covered by tests

err = layer.UnpackLayer(extractDir, uncompressed, nil)
if err != nil {
if rmErr := os.RemoveAll(extractDir); rmErr != nil {
log.Errorf("Failed to remove dir '%s' after failed extraction: %v", extractDir, rmErr)
}

Check warning on line 148 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L146-L148

Added lines #L146 - L148 were not covered by tests
}
return err
}
return errors.Errorf("unknown media type %s", l.MediaType)

Check warning on line 152 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L152

Added line #L152 was not covered by tests
}

// Unpack an image with "tag" from "ociLayout" into paths returned by "pathfunc"
func Unpack(ociLayout, tag string, pathfunc func(digest.Digest) string) (int, error) {
oci, err := umoci.OpenLayout(ociLayout)
if err != nil {
return -1, err
}

Check warning on line 160 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L159-L160

Added lines #L159 - L160 were not covered by tests
defer oci.Close()

manifest, err := LookupManifest(oci, tag)
if err != nil {
return -1, err
}

Check warning on line 166 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L165-L166

Added lines #L165 - L166 were not covered by tests

pool := NewThreadPool(runtime.NumCPU())

seen := map[digest.Digest]bool{}
for _, curLayer := range manifest.Layers {
// avoid calling UnpackOne twice for the same digest
if seen[curLayer.Digest] {
continue

Check warning on line 174 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L174

Added line #L174 was not covered by tests
}
seen[curLayer.Digest] = true

// copy layer to avoid race on pool access.
l := curLayer
pool.Add(func(ctx context.Context) error {
return UnpackOne(l, ociLayout, pathfunc(l.Digest))
})
}

pool.DoneAddingJobs()

err = pool.Run()
if err != nil {
return -1, err
}

Check warning on line 190 in pkg/oci/oci.go

View check run for this annotation

Codecov / codecov/patch

pkg/oci/oci.go#L189-L190

Added lines #L189 - L190 were not covered by tests

return len(manifest.Layers), nil
}
2 changes: 1 addition & 1 deletion pkg/overlay/pool.go → pkg/oci/pool.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package overlay
package oci

import (
"context"
Expand Down
11 changes: 2 additions & 9 deletions pkg/overlay/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
"stackerbuild.io/stacker/pkg/oci"
"stackerbuild.io/stacker/pkg/types"
)

Expand Down Expand Up @@ -142,14 +143,6 @@ func (o *overlay) SetupEmptyRootfs(name string) error {
return ovl.write(o.config, name)
}

func hasDirEntries(dir string) bool {
ents, err := os.ReadDir(dir)
if err != nil {
return false
}
return len(ents) != 0
}

func (o *overlay) snapshot(source string, target string) error {
err := o.Create(target)
if err != nil {
Expand All @@ -168,7 +161,7 @@ func (o *overlay) snapshot(source string, target string) error {
}
ociDir := path.Join(o.config.StackerDir, "layer-bases", "oci")
for _, layer := range manifest.Layers {
err := unpackOne(layer, ociDir, overlayPath(o.config.RootFSDir, layer.Digest, "overlay"))
err := oci.UnpackOne(layer, ociDir, overlayPath(o.config.RootFSDir, layer.Digest, "overlay"))
if err != nil {
return errors.Wrapf(err, "Failed mounting %#v", layer)
}
Expand Down
121 changes: 21 additions & 100 deletions pkg/overlay/pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"time"

"github.com/klauspost/pgzip"
"github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/umoci"
Expand All @@ -24,14 +21,13 @@
"github.com/pkg/xattr"
"stackerbuild.io/stacker/pkg/lib"
"stackerbuild.io/stacker/pkg/log"
"stackerbuild.io/stacker/pkg/oci"
stackeroci "stackerbuild.io/stacker/pkg/oci"
"stackerbuild.io/stacker/pkg/squashfs"
"stackerbuild.io/stacker/pkg/storage"
"stackerbuild.io/stacker/pkg/types"
)

var tarEx sync.Mutex

// Container image layers are often tar.gz, however there is nothing in the
// spec or documentation which standardizes compression params which can cause
// different layer hashes even for the same tar. So picking compression params
Expand All @@ -51,56 +47,6 @@
return path.Join(dirs...)
}

func (o *overlay) Unpack(tag, name string) error {
cacheDir := path.Join(o.config.StackerDir, "layer-bases", "oci")
oci, err := umoci.OpenLayout(cacheDir)
if err != nil {
return err
}
defer oci.Close()

manifest, err := stackeroci.LookupManifest(oci, tag)
if err != nil {
return err
}

pool := NewThreadPool(runtime.NumCPU())

seen := map[digest.Digest]bool{}
for _, curLayer := range manifest.Layers {
// avoid calling unpackOne twice for the same digest
if seen[curLayer.Digest] {
continue
}
seen[curLayer.Digest] = true

// copy layer to avoid race on pool access.
l := curLayer
pool.Add(func(ctx context.Context) error {
return unpackOne(l, cacheDir, overlayPath(o.config.RootFSDir, l.Digest, "overlay"))
})
}

pool.DoneAddingJobs()

err = pool.Run()
if err != nil {
return err
}

err = o.Create(name)
if err != nil {
return err
}

ovl, err := newOverlayMetadataFromOCI(oci, tag)
if err != nil {
return err
}

return ovl.write(o.config, name)
}

func ConvertAndOutput(config types.StackerConfig, tag, name string, layerType types.LayerType) error {
cacheDir := path.Join(config.StackerDir, "layer-bases", "oci")
cacheOCI, err := umoci.OpenLayout(cacheDir)
Expand Down Expand Up @@ -681,57 +627,32 @@
return ovl.write(config, name)
}

// unpackOne - unpack a single layer (Descriptor) found in ociDir to extractDir
//
// The result of calling unpackOne is either error or the contents available
// at the provided extractDir. The extractDir should be either empty or
// fully populated with this layer.
func unpackOne(l ispec.Descriptor, ociDir string, extractDir string) error {
// population of a dir is not atomic, at least for tar extraction.
// As a result, we could hasDirEntries(extractDir) at the same time that
// something is un-populating that dir due to a failed extraction (like
// os.RemoveAll below).
// There needs to be a lock on the extract dir (scoped to the overlay storage backend).
// A sync.RWMutex would work well here since it is safe to check as long
// as no one is populating or unpopulating.
if hasDirEntries(extractDir) {
// the directory was already populated.
return nil
}
func (o *overlay) Unpack(tag, name string) error {
cacheDir := path.Join(o.config.StackerDir, "layer-bases", "oci")

if squashfs.IsSquashfsMediaType(l.MediaType) {
return squashfs.ExtractSingleSquash(
path.Join(ociDir, "blobs", "sha256", l.Digest.Encoded()), extractDir)
pathfunc := func(digest digest.Digest) string {
return overlayPath(o.config.RootFSDir, digest, "overlay")
}
switch l.MediaType {
case ispec.MediaTypeImageLayer, ispec.MediaTypeImageLayerGzip:
tarEx.Lock()
defer tarEx.Unlock()

oci, err := umoci.OpenLayout(ociDir)
if err != nil {
return err
}
defer oci.Close()
_, err := oci.Unpack(cacheDir, tag, pathfunc)
if err != nil {
return err
}

Check warning on line 640 in pkg/overlay/pack.go

View check run for this annotation

Codecov / codecov/patch

pkg/overlay/pack.go#L639-L640

Added lines #L639 - L640 were not covered by tests

compressed, err := oci.GetBlob(context.Background(), l.Digest)
if err != nil {
return err
}
defer compressed.Close()
err = o.Create(name)
if err != nil {
return err
}

Check warning on line 645 in pkg/overlay/pack.go

View check run for this annotation

Codecov / codecov/patch

pkg/overlay/pack.go#L644-L645

Added lines #L644 - L645 were not covered by tests

uncompressed, err := pgzip.NewReader(compressed)
if err != nil {
return err
}
oci, err := umoci.OpenLayout(cacheDir)
if err != nil {
return err
}

Check warning on line 650 in pkg/overlay/pack.go

View check run for this annotation

Codecov / codecov/patch

pkg/overlay/pack.go#L649-L650

Added lines #L649 - L650 were not covered by tests

err = layer.UnpackLayer(extractDir, uncompressed, nil)
if err != nil {
if rmErr := os.RemoveAll(extractDir); rmErr != nil {
log.Errorf("Failed to remove dir '%s' after failed extraction: %v", extractDir, rmErr)
}
}
ovl, err := newOverlayMetadataFromOCI(oci, tag)
if err != nil {
return err
}
return errors.Errorf("unknown media type %s", l.MediaType)

return ovl.write(o.config, name)
}
33 changes: 33 additions & 0 deletions pkg/stacker/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"github.com/vbatts/go-mtree"
"stackerbuild.io/stacker/pkg/lib"
"stackerbuild.io/stacker/pkg/log"
"stackerbuild.io/stacker/pkg/oci"
"stackerbuild.io/stacker/pkg/types"
)

Expand Down Expand Up @@ -294,6 +295,38 @@
}

return p, nil
} else if url.Scheme == "docker" {
if idest != "" && idest[len(idest)-1:] != "/" {
return "", errors.Errorf("The destination path must be directory: %s", idest)
}

Check warning on line 301 in pkg/stacker/import.go

View check run for this annotation

Codecov / codecov/patch

pkg/stacker/import.go#L300-L301

Added lines #L300 - L301 were not covered by tests

is := types.ImageSource{Type: "docker", Url: i}
if err := importContainersImage(is, c, false); err != nil {
return "", err
}

Check warning on line 306 in pkg/stacker/import.go

View check run for this annotation

Codecov / codecov/patch

pkg/stacker/import.go#L305-L306

Added lines #L305 - L306 were not covered by tests

tag, err := is.ParseTag()
if err != nil {
return "", err
}

Check warning on line 311 in pkg/stacker/import.go

View check run for this annotation

Codecov / codecov/patch

pkg/stacker/import.go#L310-L311

Added lines #L310 - L311 were not covered by tests

pathfunc := func(digest digest.Digest) string {
_ = os.Remove(cache)
return cache
}

ociDir := path.Join(c.StackerDir, "layer-bases", "oci")

n, err := oci.Unpack(ociDir, tag, pathfunc)
if err != nil {
return "", err
}

Check warning on line 323 in pkg/stacker/import.go

View check run for this annotation

Codecov / codecov/patch

pkg/stacker/import.go#L322-L323

Added lines #L322 - L323 were not covered by tests

if n > 1 {
return "", errors.Errorf("Currently supporting single-layer container image imports")
}

Check warning on line 327 in pkg/stacker/import.go

View check run for this annotation

Codecov / codecov/patch

pkg/stacker/import.go#L326-L327

Added lines #L326 - L327 were not covered by tests

return cache, nil
}

return "", errors.Errorf("unsupported url scheme %s", i)
Expand Down
Loading
Loading