diff --git a/cmd/oci-image-tool/create.go b/cmd/oci-image-tool/create.go index ced7cce..20a233f 100644 --- a/cmd/oci-image-tool/create.go +++ b/cmd/oci-image-tool/create.go @@ -47,7 +47,9 @@ func createHandle(context *cli.Context) error { v.ref = context.String("ref") } if context.IsSet("rootfs") { - v.root = context.String("roofs") + v.root = context.String("rootfs") + } else { + v.root = "rootfs" } if v.typ == "" { @@ -64,7 +66,7 @@ func createHandle(context *cli.Context) error { err = image.CreateRuntimeBundleLayout(context.Args()[0], context.Args()[1], v.ref, v.root) case image.TypeImage: - err = image.CreateRuntimeBundle(context.Args()[0], context.Args()[1], v.ref, v.root) + err = image.CreateRuntimeBundleFile(context.Args()[0], context.Args()[1], v.ref, v.root) default: err = fmt.Errorf("cannot create %q", v.typ) diff --git a/cmd/oci-image-tool/unpack.go b/cmd/oci-image-tool/unpack.go index 76f9a68..21bd2a6 100644 --- a/cmd/oci-image-tool/unpack.go +++ b/cmd/oci-image-tool/unpack.go @@ -60,7 +60,7 @@ func unpackHandle(context *cli.Context) error { err = image.UnpackLayout(context.Args()[0], context.Args()[1], v.ref) case image.TypeImage: - err = image.Unpack(context.Args()[0], context.Args()[1], v.ref) + err = image.UnpackFile(context.Args()[0], context.Args()[1], v.ref) default: err = fmt.Errorf("cannot unpack %q", v.typ) diff --git a/cmd/oci-image-tool/validate.go b/cmd/oci-image-tool/validate.go index ed0895e..4c705df 100644 --- a/cmd/oci-image-tool/validate.go +++ b/cmd/oci-image-tool/validate.go @@ -100,7 +100,7 @@ func validatePath(name string) error { case image.TypeImageLayout: return image.ValidateLayout(name, v.refs, v.stdout) case image.TypeImage: - return image.Validate(name, v.refs, v.stdout) + return image.ValidateFile(name, v.refs, v.stdout) } if len(v.refs) != 0 { diff --git a/image/descriptor.go b/image/descriptor.go index f19642c..21bae8b 100644 --- a/image/descriptor.go +++ b/image/descriptor.go @@ -105,28 +105,23 @@ func (d *descriptor) validate(w walker, mts []string) error { return fmt.Errorf("invalid descriptor MediaType %q", d.MediaType) } - rc, err := w.Get(*d) + parsed, err := digest.Parse(d.Digest) if err != nil { return err } - defer rc.Close() - - return d.validateContent(rc) -} -func (d *descriptor) validateContent(r io.Reader) error { - parsed, err := digest.Parse(d.Digest) + // Copy the contents of the layer in to the verifier + verifier := parsed.Verifier() + numBytes, err := w.get(*d, verifier) if err != nil { return err } - verifier := parsed.Verifier() - n, err := io.Copy(verifier, r) if err != nil { return errors.Wrap(err, "error generating hash") } - if n != d.Size { + if numBytes != d.Size { return errors.New("size mismatch") } diff --git a/image/image.go b/image/image.go index ad12454..4ff345c 100644 --- a/image/image.go +++ b/image/image.go @@ -17,6 +17,7 @@ package image import ( "encoding/json" "fmt" + "io" "log" "os" "path/filepath" @@ -31,16 +32,24 @@ func ValidateLayout(src string, refs []string, out *log.Logger) error { return validate(newPathWalker(src), refs, out) } -// Validate walks through the given .tar file and validates the manifest -// pointed to by the given refs or returns an error if the validation failed. -func Validate(tarFile string, refs []string, out *log.Logger) error { +// ValidateFile opens the tar file given by the filename, then calls ValidateReader +func ValidateFile(tarFile string, refs []string, out *log.Logger) error { f, err := os.Open(tarFile) if err != nil { return errors.Wrap(err, "unable to open file") } defer f.Close() - return validate(newTarWalker(tarFile, f), refs, out) + return Validate(f, refs, out) +} + +// Validate walks through a tar stream and validates the manifest. +// * Check that all refs point to extant blobs +// * Checks that all referred blobs are valid +// * Checks that mime-types are correct +// returns error on validation failure +func Validate(r io.ReadSeeker, refs []string, out *log.Logger) error { + return validate(newTarWalker(r), refs, out) } var validRefMediaTypes = []string{ @@ -101,17 +110,23 @@ func UnpackLayout(src, dest, ref string) error { return unpack(newPathWalker(src), dest, ref) } -// Unpack walks through the given .tar file and, using the layers specified in -// the manifest pointed to by the given ref, unpacks all layers in the given -// destination directory or returns an error if the unpacking failed. -func Unpack(tarFile, dest, ref string) error { - f, err := os.Open(tarFile) +// UnpackFile opens the file pointed by tarFileName and calls Unpack on it. +func UnpackFile(tarFileName, dest, ref string) error { + f, err := os.Open(tarFileName) if err != nil { return errors.Wrap(err, "unable to open file") } defer f.Close() - return unpack(newTarWalker(tarFile, f), dest, ref) + return Unpack(f, dest, ref) +} + +// Unpack walks through the tar stream and, using the layers specified in +// the manifest pointed to by the given ref, unpacks all layers in the given +// destination directory or returns an error if the unpacking failed. +// The destination will be created if it does not exist. +func Unpack(r io.ReadSeeker, dest, refName string) error { + return unpack(newTarWalker(r), dest, refName) } func unpack(w walker, dest, refName string) error { @@ -143,17 +158,23 @@ func CreateRuntimeBundleLayout(src, dest, ref, root string) error { return createRuntimeBundle(newPathWalker(src), dest, ref, root) } -// CreateRuntimeBundle walks through the given .tar file and -// creates an OCI runtime bundle in the given destination dest -// or returns an error if the unpacking failed. -func CreateRuntimeBundle(tarFile, dest, ref, root string) error { +// CreateRuntimeBundleFile opens the file pointed by tarFile and calls +// CreateRuntimeBundle. +func CreateRuntimeBundleFile(tarFile, dest, ref, root string) error { f, err := os.Open(tarFile) if err != nil { return errors.Wrap(err, "unable to open file") } defer f.Close() - return createRuntimeBundle(newTarWalker(tarFile, f), dest, ref, root) + return createRuntimeBundle(newTarWalker(f), dest, ref, root) +} + +// CreateRuntimeBundle walks through the given tar stream and +// creates an OCI runtime bundle in the given destination dest +// or returns an error if the unpacking failed. +func CreateRuntimeBundle(r io.ReadSeeker, dest, ref, root string) error { + return createRuntimeBundle(newTarWalker(r), dest, ref, root) } func createRuntimeBundle(w walker, dest, refName, rootfs string) error { @@ -190,8 +211,7 @@ func createRuntimeBundle(w walker, dest, refName, rootfs string) error { } } - err = m.unpack(w, filepath.Join(dest, rootfs)) - if err != nil { + if err = m.unpack(w, filepath.Join(dest, rootfs)); err != nil { return err } diff --git a/image/manifest.go b/image/manifest.go index da0086c..5431ff8 100644 --- a/image/manifest.go +++ b/image/manifest.go @@ -94,8 +94,8 @@ func (m *manifest) validate(w walker) error { func (m *manifest) unpack(w walker, dest string) (retErr error) { // error out if the dest directory is not empty s, err := ioutil.ReadDir(dest) - if err != nil && !os.IsNotExist(err) { - return errors.Wrap(err, "unable to open file") // err contains dest + if err != nil && !os.IsNotExist(err) { // We'll create the dir later + return errors.Wrap(err, "unpack: unable to open dest") // err contains dest } if len(s) > 0 { return fmt.Errorf("%s is not empty", dest) @@ -121,7 +121,7 @@ func (m *manifest) unpack(w walker, dest string) (retErr error) { } if err := unpackLayer(dest, r); err != nil { - return errors.Wrap(err, "error extracting layer") + return errors.Wrap(err, "unpack: error extracting layer") } return errEOW diff --git a/image/reader.go b/image/reader.go deleted file mode 100644 index 078db5a..0000000 --- a/image/reader.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2016 The Linux Foundation -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package image - -import ( - "archive/tar" - "bytes" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" -) - -type reader interface { - Get(desc descriptor) (io.ReadCloser, error) -} - -type tarReader struct { - name string -} - -func (r *tarReader) Get(desc descriptor) (io.ReadCloser, error) { - f, err := os.Open(r.name) - if err != nil { - return nil, err - } - defer f.Close() - - tr := tar.NewReader(f) -loop: - for { - hdr, err := tr.Next() - switch err { - case io.EOF: - break loop - case nil: - // success, continue below - default: - return nil, err - } - if hdr.Name == filepath.Join("blobs", desc.algo(), desc.hash()) && - !hdr.FileInfo().IsDir() { - buf, err := ioutil.ReadAll(tr) - if err != nil { - return nil, err - } - return ioutil.NopCloser(bytes.NewReader(buf)), nil - } - } - - return nil, fmt.Errorf("object not found") -} - -type layoutReader struct { - root string -} - -func (r *layoutReader) Get(desc descriptor) (io.ReadCloser, error) { - name := filepath.Join(r.root, "blobs", desc.algo(), desc.hash()) - - info, err := os.Stat(name) - if err != nil { - return nil, err - } - - if info.IsDir() { - return nil, fmt.Errorf("object is dir") - } - - return os.Open(name) -} diff --git a/image/walker.go b/image/walker.go index 421c992..3ef5e8b 100644 --- a/image/walker.go +++ b/image/walker.go @@ -20,6 +20,7 @@ import ( "io" "os" "path/filepath" + "sync" "github.com/pkg/errors" ) @@ -31,24 +32,34 @@ var ( // walkFunc is a function type that gets called for each file or directory visited by the Walker. type walkFunc func(path string, _ os.FileInfo, _ io.Reader) error -// walker is the interface that walks through a file tree, -// calling walk for each file or directory in the tree. +// walker is the interface that defines how to access a given archival format type walker interface { + + // walk calls walkfunc for every entity in the archive walk(walkFunc) error - reader + + // get will copy an arbitrary blob, defined by desc, in to dst. returns + // the number of bytes copied on success. + get(desc descriptor, dst io.Writer) (int64, error) } +// tarWalker exposes access to image layouts in a tar file. type tarWalker struct { r io.ReadSeeker - tarReader + + // Synchronize use of the reader + mut sync.Mutex } // newTarWalker returns a Walker that walks through .tar files. -func newTarWalker(tarFile string, r io.ReadSeeker) walker { - return &tarWalker{r, tarReader{name: tarFile}} +func newTarWalker(r io.ReadSeeker) walker { + return &tarWalker{r: r} } func (w *tarWalker) walk(f walkFunc) error { + w.mut.Lock() + defer w.mut.Unlock() + if _, err := w.r.Seek(0, io.SeekStart); err != nil { return errors.Wrapf(err, "unable to reset") } @@ -76,6 +87,37 @@ loop: return nil } +func (w *tarWalker) get(desc descriptor, dst io.Writer) (int64, error) { + var bytes int64 + done := false + + expectedPath := filepath.Join("blobs", desc.algo(), desc.hash()) + + f := func(path string, info os.FileInfo, rdr io.Reader) error { + var err error + if done { + return nil + } + + if path == expectedPath && !info.IsDir() { + if bytes, err = io.Copy(dst, rdr); err != nil { + return errors.Wrapf(err, "get failed: failed to copy blob to destination") + } + done = true + } + return nil + } + + if err := w.walk(f); err != nil { + return 0, errors.Wrapf(err, "get failed: unable to walk") + } + if !done { + return 0, os.ErrNotExist + } + + return bytes, nil +} + type eofReader struct{} func (eofReader) Read(_ []byte) (int, error) { @@ -84,13 +126,12 @@ func (eofReader) Read(_ []byte) (int, error) { type pathWalker struct { root string - layoutReader } // newPathWalker returns a Walker that walks through directories // starting at the given root path. It does not follow symlinks. func newPathWalker(root string) walker { - return &pathWalker{root, layoutReader{root: root}} + return &pathWalker{root} } func (w *pathWalker) walk(f walkFunc) error { @@ -119,3 +160,28 @@ func (w *pathWalker) walk(f walkFunc) error { return f(rel, info, file) }) } + +func (w *pathWalker) get(desc descriptor, dst io.Writer) (int64, error) { + name := filepath.Join(w.root, "blobs", desc.algo(), desc.hash()) + + info, err := os.Stat(name) + if err != nil { + return 0, err + } + + if info.IsDir() { + return 0, fmt.Errorf("object is dir") + } + + fp, err := os.Open(name) + if err != nil { + return 0, errors.Wrapf(err, "get failed") + } + defer fp.Close() + + nbytes, err := io.Copy(dst, fp) + if err != nil { + return 0, errors.Wrapf(err, "get failed: failed to copy blob to destination") + } + return nbytes, nil +}