vendor: revendor github.com/opencontainers/image-tools@da84dc9dddc823a32f543e60323f841d12429c51

This requires re-vendoring a bunch of other things (as well as the old
Sirupsen/logrus path), the relevant commits being:

* github.com/xeipuuv/gojsonschema@0c8571ac0ce161a5feb57375a9cdf148c98c0f70
* github.com/xeipuuv/gojsonpointer@6fe8760cad3569743d51ddbb243b26f8456742dc
* github.com/xeipuuv/gojsonreference@e02fc20de94c78484cd5ffb007f8af96be030a45
* go4.org@034d17a462f7b2dcd1a4a73553ec5357ff6e6c6e

Signed-off-by: Aleksa Sarai <asarai@suse.de>
This commit is contained in:
Aleksa Sarai
2017-08-15 01:44:31 +10:00
parent 1d1cc1ff5b
commit 96ce8b63bc
69 changed files with 11027 additions and 1 deletions

201
vendor/github.com/opencontainers/image-tools/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

177
vendor/github.com/opencontainers/image-tools/README.md generated vendored Normal file
View File

@@ -0,0 +1,177 @@
# oci-image-tool [![Build Status](https://travis-ci.org/opencontainers/image-tools.svg?branch=master)](https://travis-ci.org/opencontainers/image-tools)[![Go Report Card](https://goreportcard.com/badge/github.com/opencontainers/image-tools)](https://goreportcard.com/report/github.com/opencontainers/image-tools)
`oci-image-tool` is a collection of tools for working with the [OCI image format specification](https://github.com/opencontainers/image-spec).
## Install
It is recommended that use `go get` to download a single command tools.
```
$ go get -d github.com/opencontainers/image-tools/cmd/oci-image-tool
$ cd $GOPATH/src/github.com/opencontainers/image-tools/
$ make all
$ sudo make install
```
## Uninstall
```
$ sudo make uninstall
```
## Example
### Obtaining an image
The following examples assume you have a [image-layout](https://github.com/opencontainers/image-spec/blob/v1.0.0-rc2/image-layout.md) tar archive at `busybox-oci`.
One way to acquire that image is with [skopeo](https://github.com/projectatomic/skopeo#installing):
```
$ skopeo copy docker://busybox oci:busybox-oci
```
### oci-image-tool-create
More information about `oci-image-tool-create` can be found in its [man page](./man/oci-image-tool-create.1.md)
```
$ mkdir busybox-bundle
$ oci-image-tool create --ref latest busybox-oci busybox-bundle
$ cd busybox-bundle && sudo runc run busybox
```
### oci-image-tool-validate
More information about `oci-image-tool-validate` can be found in its [man page](./man/oci-image-tool-validate.1.md)
```
$ oci-image-tool validate --type imageLayout --ref latest busybox-oci
busybox-oci: OK
```
### oci-image-tool-unpack
More information about `oci-image-tool-unpack` can be found in its [man page](./man/oci-image-tool-unpack.1.md)
```
$ mkdir busybox-bundle
$ oci-image-tool unpack --ref latest busybox-oci busybox-bundle
$ tree busybox-bundle
busybox-bundle
├── bin
│   ├── [
│   ├── [[
│   ├── acpid
│   ├── addgroup
│   ├── add-shell
[...]
```
# Contributing
Development happens on GitHub. Issues are used for bugs and actionable items and longer discussions can happen on the [mailing list](#mailing-list).
The code is licensed under the Apache 2.0 license found in the `LICENSE` file of this repository.
## Code of Conduct
Participation in the OpenContainers community is governed by [OpenContainer's Code of Conduct](https://github.com/opencontainers/tob/blob/d2f9d68c1332870e40693fe077d311e0742bc73d/code-of-conduct.md).
## Discuss your design
The project welcomes submissions, but please let everyone know what you are working on.
Before undertaking a nontrivial change to this repository, send mail to the [mailing list](#mailing-list) to discuss what you plan to do.
This gives everyone a chance to validate the design, helps prevent duplication of effort, and ensures that the idea fits.
It also guarantees that the design is sound before code is written; a GitHub pull-request is not the place for high-level discussions.
Typos and grammatical errors can go straight to a pull-request.
When in doubt, start on the [mailing-list](#mailing-list).
## Weekly Call
The contributors and maintainers of all OCI projects have a weekly meeting Wednesdays at 2:00 PM (USA Pacific.)
Everyone is welcome to participate via [UberConference web][UberConference] or audio-only: 888-587-9088 or 860-706-8529 (no PIN needed.)
An initial agenda will be posted to the [mailing list](#mailing-list) earlier in the week, and everyone is welcome to propose additional topics or suggest other agenda alterations there.
Minutes are posted to the [mailing list](#mailing-list) and minutes from past calls are archived to the [wiki](https://github.com/opencontainers/runtime-spec/wiki) for those who are unable to join the call.
## Mailing List
You can subscribe and join the mailing list on [Google Groups](https://groups.google.com/a/opencontainers.org/forum/#!forum/dev).
## IRC
OCI discussion happens on #opencontainers on Freenode ([logs][irc-logs]).
## Git commit
### Sign your work
The sign-off is a simple line at the end of the explanation for the patch, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch.
The rules are pretty simple: if you can certify the below (from [developercertificate.org](http://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe@gmail.com>
using your real name (sorry, no pseudonyms or anonymous contributions.)
You can add the sign off when creating the git commit via `git commit -s`.
### Commit Style
Simple house-keeping for clean git history.
Read more on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/) or the Discussion section of [`git-commit(1)`](http://git-scm.com/docs/git-commit).
1. Separate the subject from body with a blank line
2. Limit the subject line to 50 characters
3. Capitalize the subject line
4. Do not end the subject line with a period
5. Use the imperative mood in the subject line
6. Wrap the body at 72 characters
7. Use the body to explain what and why vs. how
* If there was important/useful/essential conversation or information, copy or include a reference
8. When possible, one keyword to scope the change in the subject (i.e. "README: ...", "runtime: ...")
[UberConference]: https://www.uberconference.com/opencontainers
[irc-logs]: http://ircbot.wl.linuxfoundation.org/eavesdrop/%23opencontainers/

View File

@@ -0,0 +1,109 @@
// 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 (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"os"
"github.com/opencontainers/image-spec/schema"
"github.com/pkg/errors"
)
// supported autodetection types
const (
TypeImageLayout = "imageLayout"
TypeImage = "image"
TypeManifest = "manifest"
TypeImageIndex = "imageIndex"
TypeConfig = "config"
)
// Autodetect detects the validation type for the given path
// or an error if the validation type could not be resolved.
func Autodetect(path string) (string, error) {
fi, err := os.Stat(path)
if err != nil {
return "", errors.Wrapf(err, "unable to access path") // err from os.Stat includes path name
}
if fi.IsDir() {
return TypeImageLayout, nil
}
f, err := os.Open(path)
if err != nil {
return "", errors.Wrap(err, "unable to open file") // os.Open includes the filename
}
defer f.Close()
buf, err := ioutil.ReadAll(io.LimitReader(f, 512)) // read some initial bytes to detect content
if err != nil {
return "", errors.Wrap(err, "unable to read")
}
mimeType := http.DetectContentType(buf)
switch mimeType {
case "application/x-gzip", "application/octet-stream":
return TypeImage, nil
case "text/plain; charset=utf-8":
// might be a JSON file, will be handled below
default:
return "", errors.New("unknown file type")
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
return "", errors.Wrap(err, "unable to seek")
}
header := struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
Config interface{} `json:"config"`
}{}
if err := json.NewDecoder(f).Decode(&header); err != nil {
if _, errSeek := f.Seek(0, io.SeekStart); errSeek != nil {
return "", errors.Wrap(err, "unable to seek")
}
e := errors.Wrap(
schema.WrapSyntaxError(f, err),
"unable to parse JSON",
)
return "", e
}
switch {
case header.MediaType == string(schema.ValidatorMediaTypeManifest):
return TypeManifest, nil
case header.MediaType == string(schema.ValidatorMediaTypeImageIndex):
return TypeImageIndex, nil
case header.MediaType == "" && header.SchemaVersion == 0 && header.Config != nil:
// config files don't have mediaType/schemaVersion header
return TypeConfig, nil
}
return "", errors.New("unknown media type")
}

View File

@@ -0,0 +1,126 @@
// 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 (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/opencontainers/image-spec/schema"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
type config v1.Image
func findConfig(w walker, d *v1.Descriptor) (*config, error) {
var c config
cpath := filepath.Join("blobs", string(d.Digest.Algorithm()), d.Digest.Hex())
switch err := w.walk(func(path string, info os.FileInfo, r io.Reader) error {
if info.IsDir() || filepath.Clean(path) != cpath {
return nil
}
buf, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrapf(err, "%s: error reading config", path)
}
if err := schema.ValidatorMediaTypeImageConfig.Validate(bytes.NewReader(buf)); err != nil {
return errors.Wrapf(err, "%s: config validation failed", path)
}
if err := json.Unmarshal(buf, &c); err != nil {
return err
}
return errEOW
}); err {
case nil:
return nil, fmt.Errorf("%s: config not found", cpath)
case errEOW:
return &c, nil
default:
return nil, err
}
}
func (c *config) runtimeSpec(rootfs string) (*specs.Spec, error) {
if c.OS != "linux" {
return nil, fmt.Errorf("%s: unsupported OS", c.OS)
}
var s specs.Spec
s.Version = specs.Version
// we should at least apply the default spec, otherwise this is totally useless
s.Root = &specs.Root{}
s.Root.Path = rootfs
s.Process = &specs.Process{}
s.Process.Terminal = true
s.Process.Cwd = "/"
if c.Config.WorkingDir != "" {
s.Process.Cwd = c.Config.WorkingDir
}
s.Process.Env = append(s.Process.Env, c.Config.Env...)
s.Process.Args = append(s.Process.Args, c.Config.Entrypoint...)
s.Process.Args = append(s.Process.Args, c.Config.Cmd...)
if len(s.Process.Args) == 0 {
s.Process.Args = append(s.Process.Args, "sh")
}
if uid, err := strconv.Atoi(c.Config.User); err == nil {
s.Process.User.UID = uint32(uid)
} else if ug := strings.Split(c.Config.User, ":"); len(ug) == 2 {
uid, err := strconv.Atoi(ug[0])
if err != nil {
return nil, errors.New("config.User: unsupported uid format")
}
gid, err := strconv.Atoi(ug[1])
if err != nil {
return nil, errors.New("config.User: unsupported gid format")
}
s.Process.User.UID = uint32(uid)
s.Process.User.GID = uint32(gid)
} else if c.Config.User != "" {
return nil, errors.New("config.User: unsupported format")
}
s.Linux = &specs.Linux{}
for vol := range c.Config.Volumes {
s.Mounts = append(
s.Mounts,
specs.Mount{
Destination: vol,
Type: "bind",
Options: []string{"rbind"},
},
)
}
return &s, nil
}

View File

@@ -0,0 +1,123 @@
// 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 (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
const indexPath = "index.json"
func listReferences(w walker) (map[string]*v1.Descriptor, error) {
refs := make(map[string]*v1.Descriptor)
var index v1.Index
if err := w.walk(func(path string, info os.FileInfo, r io.Reader) error {
if info.IsDir() || filepath.Clean(path) != indexPath {
return nil
}
if err := json.NewDecoder(r).Decode(&index); err != nil {
return err
}
for i := 0; i < len(index.Manifests); i++ {
if index.Manifests[i].Annotations[v1.AnnotationRefName] != "" {
refs[index.Manifests[i].Annotations[v1.AnnotationRefName]] = &index.Manifests[i]
}
}
return nil
}); err != nil {
return nil, err
}
return refs, nil
}
func findDescriptor(w walker, name string) (*v1.Descriptor, error) {
var d v1.Descriptor
var index v1.Index
switch err := w.walk(func(path string, info os.FileInfo, r io.Reader) error {
if info.IsDir() || filepath.Clean(path) != indexPath {
return nil
}
if err := json.NewDecoder(r).Decode(&index); err != nil {
return err
}
for i := 0; i < len(index.Manifests); i++ {
if index.Manifests[i].Annotations[v1.AnnotationRefName] == name {
d = index.Manifests[i]
return errEOW
}
}
return nil
}); err {
case nil:
return nil, fmt.Errorf("index.json: descriptor %q not found", name)
case errEOW:
return &d, nil
default:
return nil, err
}
}
func validateDescriptor(d *v1.Descriptor, w walker, mts []string) error {
var found bool
for _, mt := range mts {
if d.MediaType == mt {
found = true
break
}
}
if !found {
return fmt.Errorf("invalid descriptor MediaType %q", d.MediaType)
}
if err := d.Digest.Validate(); err != nil {
return err
}
// Copy the contents of the layer in to the verifier
verifier := d.Digest.Verifier()
numBytes, err := w.get(*d, verifier)
if err != nil {
return err
}
if err != nil {
return errors.Wrap(err, "error generating hash")
}
if numBytes != d.Size {
return errors.New("size mismatch")
}
if !verifier.Verified() {
return errors.New("digest mismatch")
}
return nil
}

View File

@@ -0,0 +1,16 @@
// 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 defines methods for validating, and unpacking OCI images.
package image

View File

@@ -0,0 +1,360 @@
// 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 (
"encoding/json"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// ValidateLayout walks through the given file tree and validates the manifest
// pointed to by the given refs or returns an error if the validation failed.
func ValidateLayout(src string, refs []string, out *log.Logger) error {
return validate(newPathWalker(src), refs, out)
}
// 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(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{
v1.MediaTypeImageManifest,
v1.MediaTypeImageIndex,
}
func validate(w walker, refs []string, out *log.Logger) error {
if err := layoutValidate(w); err != nil {
return err
}
ds, err := listReferences(w)
if err != nil {
return err
}
if len(refs) == 0 && len(ds) == 0 {
// TODO(runcom): ugly, we'll need a better way and library
// to express log levels.
// see https://github.com/opencontainers/image-spec/issues/288
out.Print("WARNING: no descriptors found")
}
if len(refs) == 0 {
for ref := range ds {
refs = append(refs, ref)
}
}
for _, ref := range refs {
d, ok := ds[ref]
if !ok {
// TODO(runcom):
// soften this error to a warning if the user didn't ask for any specific reference
// with --ref but she's just validating the whole image.
return fmt.Errorf("reference %s not found", ref)
}
if err = validateDescriptor(d, w, validRefMediaTypes); err != nil {
return err
}
if d.MediaType == validRefMediaTypes[0] {
m, err := findManifest(w, d)
if err != nil {
return err
}
if err := m.validate(w); err != nil {
return err
}
}
if d.MediaType == validRefMediaTypes[1] {
index, err := findIndex(w, d)
if err != nil {
return err
}
if err := validateIndex(index, w); err != nil {
return err
}
if len(index.Manifests) == 0 {
fmt.Println("warning: no manifests found")
return nil
}
for _, manifest := range index.Manifests {
m, err := findManifest(w, &manifest)
if err != nil {
return err
}
if err := m.validate(w); err != nil {
return err
}
}
}
if out != nil {
out.Printf("reference %q: OK", ref)
}
}
return nil
}
// UnpackLayout walks through the file tree given by src 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 UnpackLayout(src, dest, ref string, platform string) error {
return unpack(newPathWalker(src), dest, ref, platform)
}
// UnpackFile opens the file pointed by tarFileName and calls Unpack on it.
func UnpackFile(tarFileName, dest, ref string, platform string) error {
f, err := os.Open(tarFileName)
if err != nil {
return errors.Wrap(err, "unable to open file")
}
defer f.Close()
return Unpack(f, dest, ref, platform)
}
// 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, platform string) error {
return unpack(newTarWalker(r), dest, refName, platform)
}
func unpack(w walker, dest, refName string, platform string) error {
if err := layoutValidate(w); err != nil {
return err
}
ref, err := findDescriptor(w, refName)
if err != nil {
return err
}
if err = validateDescriptor(ref, w, validRefMediaTypes); err != nil {
return err
}
if ref.MediaType == validRefMediaTypes[0] {
m, err := findManifest(w, ref)
if err != nil {
return err
}
if err := m.validate(w); err != nil {
return err
}
return m.unpack(w, dest)
}
if ref.MediaType == validRefMediaTypes[1] {
index, err := findIndex(w, ref)
if err != nil {
return err
}
if err = validateIndex(index, w); err != nil {
return err
}
manifests, err := filterManifest(w, index.Manifests, platform)
if err != nil {
return err
}
for _, m := range manifests {
return m.unpack(w, dest)
}
}
return nil
}
// CreateRuntimeBundleLayout walks through the file tree given by src and
// creates an OCI runtime bundle in the given destination dest
// or returns an error if the unpacking failed.
func CreateRuntimeBundleLayout(src, dest, ref, root string, platform string) error {
return createRuntimeBundle(newPathWalker(src), dest, ref, root, platform)
}
// CreateRuntimeBundleFile opens the file pointed by tarFile and calls
// CreateRuntimeBundle.
func CreateRuntimeBundleFile(tarFile, dest, ref, root string, platform string) error {
f, err := os.Open(tarFile)
if err != nil {
return errors.Wrap(err, "unable to open file")
}
defer f.Close()
return createRuntimeBundle(newTarWalker(f), dest, ref, root, platform)
}
// 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, platform string) error {
return createRuntimeBundle(newTarWalker(r), dest, ref, root, platform)
}
func createRuntimeBundle(w walker, dest, refName, rootfs string, platform string) error {
if err := layoutValidate(w); err != nil {
return err
}
ref, err := findDescriptor(w, refName)
if err != nil {
return err
}
if err = validateDescriptor(ref, w, validRefMediaTypes); err != nil {
return err
}
if ref.MediaType == validRefMediaTypes[0] {
m, err := findManifest(w, ref)
if err != nil {
return err
}
if err := m.validate(w); err != nil {
return err
}
return createBundle(w, m, dest, rootfs)
}
if ref.MediaType == validRefMediaTypes[1] {
index, err := findIndex(w, ref)
if err != nil {
return err
}
if err = validateIndex(index, w); err != nil {
return err
}
manifests, err := filterManifest(w, index.Manifests, platform)
if err != nil {
return err
}
for _, m := range manifests {
return createBundle(w, m, dest, rootfs)
}
}
return nil
}
func createBundle(w walker, m *manifest, dest, rootfs string) error {
c, err := findConfig(w, &m.Config)
if err != nil {
return err
}
if _, err = os.Stat(dest); err != nil {
if os.IsNotExist(err) {
if err2 := os.MkdirAll(dest, 0755); err2 != nil {
return err2
}
} else {
return err
}
}
if err = m.unpack(w, filepath.Join(dest, rootfs)); err != nil {
return err
}
spec, err := c.runtimeSpec(rootfs)
if err != nil {
return err
}
f, err := os.Create(filepath.Join(dest, "config.json"))
if err != nil {
return err
}
defer f.Close()
return json.NewEncoder(f).Encode(spec)
}
// filertManifest returns a filtered list of manifests
func filterManifest(w walker, Manifests []v1.Descriptor, platform string) ([]*manifest, error) {
var manifests []*manifest
argsParts := strings.Split(platform, ":")
if len(argsParts) != 2 {
return manifests, fmt.Errorf("platform must have os and arch when reftype is index")
}
if len(Manifests) == 0 {
fmt.Println("warning: no manifests found")
return manifests, nil
}
for _, manifest := range Manifests {
m, err := findManifest(w, &manifest)
if err != nil {
return manifests, err
}
if err := m.validate(w); err != nil {
return manifests, err
}
if strings.EqualFold(manifest.Platform.OS, argsParts[0]) && strings.EqualFold(manifest.Platform.Architecture, argsParts[1]) {
manifests = append(manifests, m)
}
}
if len(manifests) == 0 {
return manifests, fmt.Errorf("There is no matching manifest")
}
return manifests, nil
}

View File

@@ -0,0 +1,71 @@
// 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 (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/opencontainers/image-spec/schema"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
func findIndex(w walker, d *v1.Descriptor) (*v1.Index, error) {
var index v1.Index
ipath := filepath.Join("blobs", string(d.Digest.Algorithm()), d.Digest.Hex())
switch err := w.walk(func(path string, info os.FileInfo, r io.Reader) error {
if info.IsDir() || filepath.Clean(path) != ipath {
return nil
}
buf, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrapf(err, "%s: error reading index", path)
}
if err := schema.ValidatorMediaTypeImageIndex.Validate(bytes.NewReader(buf)); err != nil {
return errors.Wrapf(err, "%s: index validation failed", path)
}
if err := json.Unmarshal(buf, &index); err != nil {
return err
}
return errEOW
}); err {
case errEOW:
return &index, nil
case nil:
return nil, fmt.Errorf("index not found")
default:
return nil, err
}
}
func validateIndex(index *v1.Index, w walker) error {
for _, manifest := range index.Manifests {
if err := validateDescriptor(&manifest, w, []string{v1.MediaTypeImageManifest}); err != nil {
return errors.Wrap(err, "manifest validation failed")
}
}
return nil
}

View File

@@ -0,0 +1,104 @@
// 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 (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/opencontainers/image-spec/schema"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
func layoutValidate(w walker) error {
var blobsExist, indexExist, layoutExist bool
if err := w.walk(func(path string, info os.FileInfo, r io.Reader) error {
if strings.EqualFold(path, "blobs") {
blobsExist = true
if !info.IsDir() {
return fmt.Errorf("blobs is not a directory")
}
return nil
}
if strings.EqualFold(path, "index.json") {
indexExist = true
if info.IsDir() {
return fmt.Errorf("index.json is a directory")
}
var index v1.Index
buf, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "error reading index.json")
}
if err := json.Unmarshal(buf, &index); err != nil {
return errors.Wrap(err, "index.json format mismatch")
}
return nil
}
if strings.EqualFold(path, "oci-layout") {
layoutExist = true
if info.IsDir() {
return fmt.Errorf("oci-layout is a directory")
}
var imageLayout v1.ImageLayout
buf, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "error reading oci-layout")
}
if err := schema.ValidatorMediaTypeLayoutHeader.Validate(bytes.NewReader(buf)); err != nil {
return errors.Wrap(err, "oci-layout: imageLayout validation failed")
}
if err := json.Unmarshal(buf, &imageLayout); err != nil {
return errors.Wrap(err, "oci-layout format mismatch")
}
return nil
}
return nil
}); err != nil {
return err
}
if !blobsExist {
return fmt.Errorf("image layout must contain blobs directory")
}
if !indexExist {
return fmt.Errorf("image layout must contain index.json file")
}
if !layoutExist {
return fmt.Errorf("image layout must contain oci-layout file")
}
return nil
}

View File

@@ -0,0 +1,317 @@
// 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"
"bufio"
"bytes"
"compress/bzip2"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/opencontainers/image-spec/schema"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
type manifest struct {
Config v1.Descriptor `json:"config"`
Layers []v1.Descriptor `json:"layers"`
}
func findManifest(w walker, d *v1.Descriptor) (*manifest, error) {
var m manifest
mpath := filepath.Join("blobs", string(d.Digest.Algorithm()), d.Digest.Hex())
switch err := w.walk(func(path string, info os.FileInfo, r io.Reader) error {
if info.IsDir() || filepath.Clean(path) != mpath {
return nil
}
buf, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrapf(err, "%s: error reading manifest", path)
}
if err := schema.ValidatorMediaTypeManifest.Validate(bytes.NewReader(buf)); err != nil {
return errors.Wrapf(err, "%s: manifest validation failed", path)
}
if err := json.Unmarshal(buf, &m); err != nil {
return err
}
return errEOW
}); err {
case nil:
return nil, fmt.Errorf("%s: manifest not found", mpath)
case errEOW:
return &m, nil
default:
return nil, err
}
}
func (m *manifest) validate(w walker) error {
if err := validateDescriptor(&m.Config, w, []string{v1.MediaTypeImageConfig}); err != nil {
return errors.Wrap(err, "config validation failed")
}
validLayerMediaTypes := []string{
v1.MediaTypeImageLayer,
v1.MediaTypeImageLayerGzip,
v1.MediaTypeImageLayerNonDistributable,
v1.MediaTypeImageLayerNonDistributableGzip,
}
for _, d := range m.Layers {
if err := validateDescriptor(&d, w, validLayerMediaTypes); err != nil {
return errors.Wrap(err, "layer validation failed")
}
}
return nil
}
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) { // 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)
}
defer func() {
// if we encounter error during unpacking
// clean up the partially-unpacked destination
if retErr != nil {
if err := os.RemoveAll(dest); err != nil {
fmt.Printf("Error: failed to remove partially-unpacked destination %v", err)
}
}
}()
for _, d := range m.Layers {
switch err := w.walk(func(path string, info os.FileInfo, r io.Reader) error {
if info.IsDir() {
return nil
}
dd, err := filepath.Rel(filepath.Join("blobs", string(d.Digest.Algorithm())), filepath.Clean(path))
if err != nil || d.Digest.Hex() != dd {
return nil
}
if err := unpackLayer(d.MediaType, path, dest, r); err != nil {
return errors.Wrap(err, "error unpack: extracting layer")
}
return errEOW
}); err {
case nil:
return fmt.Errorf("%s: layer not found", dest)
case errEOW:
default:
return err
}
}
return nil
}
func getReader(path, mediaType, comp string, buf io.Reader) (io.Reader, error) {
switch comp {
case "gzip":
if !strings.HasSuffix(mediaType, "+gzip") {
logrus.Debugf("%q: %s media type with non-%s file", path, comp, comp)
}
return gzip.NewReader(buf)
case "bzip2":
if !strings.HasSuffix(mediaType, "+bzip2") {
logrus.Debugf("%q: %s media type with non-%s file", path, comp, comp)
}
return bzip2.NewReader(buf), nil
case "xz":
return nil, errors.New("xz layers are not supported")
default:
if strings.Contains(mediaType, "+") {
logrus.Debugf("%q: %s media type with non-%s file", path, comp, comp)
}
return buf, nil
}
}
// DetectCompression detects the compression algorithm of the source.
func DetectCompression(r *bufio.Reader) (string, error) {
source, err := r.Peek(10)
if err != nil {
return "", err
}
for compression, m := range map[string][]byte{
"bzip2": {0x42, 0x5A, 0x68},
"gzip": {0x1F, 0x8B, 0x08},
// FIXME needs decompression support
// "xz": {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00},
} {
if len(source) < len(m) {
logrus.Debug("Len too short")
continue
}
if bytes.Equal(m, source[:len(m)]) {
return compression, nil
}
}
return "plain", nil
}
func unpackLayer(mediaType, path, dest string, r io.Reader) error {
entries := make(map[string]bool)
buf := bufio.NewReader(r)
comp, err := DetectCompression(buf)
if err != nil {
return err
}
reader, err := getReader(path, mediaType, comp, buf)
if err != nil {
return err
}
var dirs []*tar.Header
tr := tar.NewReader(reader)
loop:
for {
hdr, err := tr.Next()
switch err {
case io.EOF:
break loop
case nil:
// success, continue below
default:
return errors.Wrapf(err, "error advancing tar stream")
}
hdr.Name = filepath.Clean(hdr.Name)
if !strings.HasSuffix(hdr.Name, string(os.PathSeparator)) {
// Not the root directory, ensure that the parent directory exists
parent := filepath.Dir(hdr.Name)
parentPath := filepath.Join(dest, parent)
if _, err2 := os.Lstat(parentPath); err2 != nil && os.IsNotExist(err2) {
if err3 := os.MkdirAll(parentPath, 0755); err3 != nil {
return err3
}
}
}
path := filepath.Join(dest, hdr.Name)
if entries[path] {
return fmt.Errorf("duplicate entry for %s", path)
}
entries[path] = true
rel, err := filepath.Rel(dest, path)
if err != nil {
return err
}
info := hdr.FileInfo()
if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return fmt.Errorf("%q is outside of %q", hdr.Name, dest)
}
if strings.HasPrefix(info.Name(), ".wh.") {
path = strings.Replace(path, ".wh.", "", 1)
if err := os.RemoveAll(path); err != nil {
return errors.Wrap(err, "unable to delete whiteout path")
}
continue loop
}
switch hdr.Typeflag {
case tar.TypeDir:
if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) {
if err2 := os.MkdirAll(path, info.Mode()); err2 != nil {
return errors.Wrap(err2, "error creating directory")
}
}
case tar.TypeReg, tar.TypeRegA:
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, info.Mode())
if err != nil {
return errors.Wrap(err, "unable to open file")
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return errors.Wrap(err, "unable to copy")
}
f.Close()
case tar.TypeLink:
target := filepath.Join(dest, hdr.Linkname)
if !strings.HasPrefix(target, dest) {
return fmt.Errorf("invalid hardlink %q -> %q", target, hdr.Linkname)
}
if err := os.Link(target, path); err != nil {
return err
}
case tar.TypeSymlink:
target := filepath.Join(filepath.Dir(path), hdr.Linkname)
if !strings.HasPrefix(target, dest) {
return fmt.Errorf("invalid symlink %q -> %q", path, hdr.Linkname)
}
if err := os.Symlink(hdr.Linkname, path); err != nil {
return err
}
case tar.TypeXGlobalHeader:
return nil
}
// Directory mtimes must be handled at the end to avoid further
// file creation in them to modify the directory mtime
if hdr.Typeflag == tar.TypeDir {
dirs = append(dirs, hdr)
}
}
for _, hdr := range dirs {
path := filepath.Join(dest, hdr.Name)
finfo := hdr.FileInfo()
// I believe the old version was using time.Now().UTC() to overcome an
// invalid error from chtimes.....but here we lose hdr.AccessTime like this...
if err := os.Chtimes(path, time.Now().UTC(), finfo.ModTime()); err != nil {
return errors.Wrap(err, "error changing time")
}
}
return nil
}

View File

@@ -0,0 +1,21 @@
// 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
// SpecURL is the URL for the image-spec repository
var SpecURL = "https://github.com/opencontainers/image-spec"
// IssuesURL is the URL for the issues of image-tools
var IssuesURL = "https://github.com/opencontainers/image-tools/issues"

View File

@@ -0,0 +1,188 @@
// 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"
"fmt"
"io"
"os"
"path/filepath"
"sync"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
var (
errEOW = fmt.Errorf("end of walk") // error to signal stop walking
)
// 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 defines how to access a given archival format
type walker interface {
// walk calls walkfunc for every entity in the archive
walk(walkFunc) error
// get will copy an arbitrary blob, defined by desc, in to dst. returns
// the number of bytes copied on success.
get(desc v1.Descriptor, dst io.Writer) (int64, error)
}
// tarWalker exposes access to image layouts in a tar file.
type tarWalker struct {
r io.ReadSeeker
// Synchronize use of the reader
mut sync.Mutex
}
// newTarWalker returns a Walker that walks through .tar files.
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")
}
tr := tar.NewReader(w.r)
loop:
for {
hdr, err := tr.Next()
switch err {
case io.EOF:
break loop
case nil:
// success, continue below
default:
return errors.Wrapf(err, "error advancing tar stream")
}
info := hdr.FileInfo()
if err := f(hdr.Name, info, tr); err != nil {
return err
}
}
return nil
}
func (w *tarWalker) get(desc v1.Descriptor, dst io.Writer) (int64, error) {
var bytes int64
done := false
expectedPath := filepath.Join("blobs", string(desc.Digest.Algorithm()), desc.Digest.Hex())
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) {
return 0, io.EOF
}
type pathWalker struct {
root string
}
// 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}
}
func (w *pathWalker) walk(f walkFunc) error {
return filepath.Walk(w.root, func(path string, info os.FileInfo, err error) error {
// MUST check error value, to make sure the `os.FileInfo` is available.
// Otherwise panic risk will exist.
if err != nil {
return errors.Wrap(err, "error walking path")
}
rel, err := filepath.Rel(w.root, path)
if err != nil {
return errors.Wrap(err, "error walking path") // err from filepath.Walk includes path name
}
if info.IsDir() { // behave like a tar reader for directories
return f(rel, info, eofReader{})
}
file, err := os.Open(path)
if err != nil {
return errors.Wrap(err, "unable to open file") // os.Open includes the path
}
defer file.Close()
return f(rel, info, file)
})
}
func (w *pathWalker) get(desc v1.Descriptor, dst io.Writer) (int64, error) {
name := filepath.Join(w.root, "blobs", string(desc.Digest.Algorithm()), desc.Digest.Hex())
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
}