diff --git a/cmd/oci-image-tool/create.go b/cmd/oci-image-tool/create.go index d1a9e48..be02286 100644 --- a/cmd/oci-image-tool/create.go +++ b/cmd/oci-image-tool/create.go @@ -29,9 +29,10 @@ var bundleTypes = []string{ } type bundleCmd struct { - typ string // the type to bundle, can be empty string - ref string - root string + typ string // the type to bundle, can be empty string + ref string + root string + platform string } func createHandle(context *cli.Context) error { @@ -40,9 +41,10 @@ func createHandle(context *cli.Context) error { } v := bundleCmd{ - typ: context.String("type"), - ref: context.String("ref"), - root: context.String("rootfs"), + typ: context.String("type"), + ref: context.String("ref"), + root: context.String("rootfs"), + platform: context.String("platform"), } if v.typ == "" { @@ -56,10 +58,10 @@ func createHandle(context *cli.Context) error { var err error switch v.typ { case image.TypeImageLayout: - err = image.CreateRuntimeBundleLayout(context.Args()[0], context.Args()[1], v.ref, v.root) + err = image.CreateRuntimeBundleLayout(context.Args()[0], context.Args()[1], v.ref, v.root, v.platform) case image.TypeImage: - err = image.CreateRuntimeBundleFile(context.Args()[0], context.Args()[1], v.ref, v.root) + err = image.CreateRuntimeBundleFile(context.Args()[0], context.Args()[1], v.ref, v.root, v.platform) default: err = fmt.Errorf("cannot create %q", v.typ) @@ -95,5 +97,9 @@ var createCommand = cli.Command{ Value: "rootfs", Usage: "A directory representing the root filesystem of the container in the OCI runtime bundle. It is strongly recommended to keep the default value.", }, + cli.StringFlag{ + Name: "platform", + Usage: "Specify the os and architecture of the manifest, format is OS:Architecture. Only applicable if reftype is index.", + }, }, } diff --git a/cmd/oci-image-tool/unpack.go b/cmd/oci-image-tool/unpack.go index 7144dd2..ab5c817 100644 --- a/cmd/oci-image-tool/unpack.go +++ b/cmd/oci-image-tool/unpack.go @@ -29,8 +29,9 @@ var unpackTypes = []string{ } type unpackCmd struct { - typ string // the type to unpack, can be empty string - ref string + typ string // the type to unpack, can be empty string + ref string + platform string } func unpackHandle(context *cli.Context) error { @@ -39,8 +40,9 @@ func unpackHandle(context *cli.Context) error { } v := unpackCmd{ - typ: context.String("type"), - ref: context.String("ref"), + typ: context.String("type"), + ref: context.String("ref"), + platform: context.String("platform"), } if v.typ == "" { @@ -54,10 +56,10 @@ func unpackHandle(context *cli.Context) error { var err error switch v.typ { case image.TypeImageLayout: - err = image.UnpackLayout(context.Args()[0], context.Args()[1], v.ref) + err = image.UnpackLayout(context.Args()[0], context.Args()[1], v.ref, v.platform) case image.TypeImage: - err = image.UnpackFile(context.Args()[0], context.Args()[1], v.ref) + err = image.UnpackFile(context.Args()[0], context.Args()[1], v.ref, v.platform) default: err = fmt.Errorf("cannot unpack %q", v.typ) @@ -86,5 +88,9 @@ var unpackCommand = cli.Command{ Value: "v1.0", Usage: "The ref pointing to the manifest of the OCI image. This must be present in the 'refs' subdirectory of the image.", }, + cli.StringFlag{ + Name: "platform", + Usage: "Specify the os and architecture of the manifest, format is OS:Architecture. Only applicable if reftype is index.", + }, }, } diff --git a/completions/bash/oci-image-tool b/completions/bash/oci-image-tool index d796aa2..847dfed 100644 --- a/completions/bash/oci-image-tool +++ b/completions/bash/oci-image-tool @@ -150,7 +150,7 @@ _oci-image-tool_create() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--type --ref --rootfs --help -h" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--type --ref --rootfs --platform --help -h" -- "$cur" ) ) ;; esac @@ -166,7 +166,7 @@ _oci-image-tool_unpack() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--type --ref --help -h" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--type --ref --platform --help -h" -- "$cur" ) ) ;; esac diff --git a/image/descriptor.go b/image/descriptor.go index dd04d2c..290bcf9 100644 --- a/image/descriptor.go +++ b/image/descriptor.go @@ -25,12 +25,14 @@ import ( "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) != "index.json" { + if info.IsDir() || filepath.Clean(path) != indexPath { return nil } @@ -56,7 +58,7 @@ func findDescriptor(w walker, name string) (*v1.Descriptor, error) { var index v1.Index switch err := w.walk(func(path string, info os.FileInfo, r io.Reader) error { - if info.IsDir() || filepath.Clean(path) != "index.json" { + if info.IsDir() || filepath.Clean(path) != indexPath { return nil } diff --git a/image/image.go b/image/image.go index a92ea23..e7ddd54 100644 --- a/image/image.go +++ b/image/image.go @@ -21,6 +21,7 @@ import ( "log" "os" "path/filepath" + "strings" "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -92,14 +93,44 @@ func validate(w walker, refs []string, out *log.Logger) error { return err } - m, err := findManifest(w, d) - if 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 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) } @@ -110,30 +141,30 @@ func validate(w walker, refs []string, out *log.Logger) error { // 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) error { - return unpack(newPathWalker(src), dest, ref) +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) error { +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) + 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) error { - return unpack(newTarWalker(r), dest, refName) +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) error { +func unpack(w walker, dest, refName string, platform string) error { if err := layoutValidate(w); err != nil { return err } @@ -147,45 +178,69 @@ func unpack(w walker, dest, refName string) error { return err } - m, err := findManifest(w, ref) - if 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 err = m.validate(w); err != nil { - return err + 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 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) error { - return createRuntimeBundle(newPathWalker(src), dest, ref, root) +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) error { +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) + 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) error { - return createRuntimeBundle(newTarWalker(r), dest, ref, root) +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) error { +func createRuntimeBundle(w walker, dest, refName, rootfs string, platform string) error { if err := layoutValidate(w); err != nil { return err } @@ -199,15 +254,43 @@ func createRuntimeBundle(w walker, dest, refName, rootfs string) error { return err } - m, err := findManifest(w, ref) - if 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 err = m.validate(w); err != nil { - return err + 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 @@ -240,3 +323,38 @@ func createRuntimeBundle(w walker, dest, refName, rootfs string) error { 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 +} diff --git a/image/image_test.go b/image/image_test.go index 61ec39e..f8d12a1 100644 --- a/image/image_test.go +++ b/image/image_test.go @@ -31,7 +31,6 @@ import ( ) const ( - refTag = "latest" layoutStr = `{"imageLayoutVersion": "1.0.0"}` configStr = `{ @@ -40,7 +39,6 @@ const ( "architecture": "amd64", "os": "linux", "config": { - "User": "alice", "ExposedPorts": { "8080/tcp": {} }, @@ -90,17 +88,43 @@ const ( ) var ( + refTag = []string{ + "latest", + "v1.0", + } + + indexJSON = `{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": , + "digest": "", + "annotations": { + "org.opencontainers.ref.name": "v1.0" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": , + "digest": "", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "annotations": { + "org.opencontainers.ref.name": "latest" + } + } + ], + "annotations": { + "com.example.index.revision": "r124356" + } +} + ` indexStr = `{ "schemaVersion": 2, "manifests": [ - { - "mediaType": "application/vnd.oci.image.index.v1+json", - "size": , - "digest": "", - "annotations": { - "org.opencontainers.ref.name": "v1.0" - } - }, { "mediaType": "application/vnd.oci.image.manifest.v1+json", "size": , @@ -108,18 +132,15 @@ var ( "platform": { "architecture": "ppc64le", "os": "linux" - }, - "annotations": { - "org.opencontainers.ref.name": "latest" } }, { - "mediaType": "application/xml", + "mediaType": "application/vnd.oci.image.manifest.v1+json", "size": , "digest": "", - "annotations": { - "org.freedesktop.specifications.metainfo.version": "1.0", - "org.freedesktop.specifications.metainfo.type": "AppStream" + "platform": { + "architecture": "amd64", + "os": "linux" } } ], @@ -129,10 +150,10 @@ var ( } ` manifestStr = `{ - "annotations": { + "annotations": { "org.freedesktop.specifications.metainfo.version": "1.0", "org.freedesktop.specifications.metainfo.type": "AppStream" - }, + }, "config": { "digest": "", "mediaType": "application/vnd.oci.image.config.v1+json", @@ -156,12 +177,14 @@ type tarContent struct { } type imageLayout struct { - rootDir string - layout string - ref string - manifest string - config string - tarList []tarContent + rootDir string + layout string + ref []string + manifest string + index string + config string + indexjson string + tarList []tarContent } func TestValidateLayout(t *testing.T) { @@ -172,11 +195,13 @@ func TestValidateLayout(t *testing.T) { defer os.RemoveAll(root) il := imageLayout{ - rootDir: root, - layout: layoutStr, - ref: refTag, - manifest: manifestStr, - config: configStr, + rootDir: root, + layout: layoutStr, + ref: refTag, + manifest: manifestStr, + index: indexStr, + indexjson: indexJSON, + config: configStr, tarList: []tarContent{ {&tar.Header{Name: "test", Size: 4, Mode: 0600}, []byte("test")}, }, @@ -188,7 +213,7 @@ func TestValidateLayout(t *testing.T) { t.Fatal(err) } - err = ValidateLayout(root, []string{refTag}, nil) + err = ValidateLayout(root, refTag, nil) if err != nil { t.Fatal(err) } @@ -226,8 +251,22 @@ func createImageLayoutBundle(il imageLayout) error { if err != nil { return err } + il.index = strings.Replace(il.index, "", string(desc.Digest), -1) + il.index = strings.Replace(il.index, "", strconv.FormatInt(desc.Size, 10), -1) + + il.indexjson = strings.Replace(il.indexjson, "", string(desc.Digest), -1) + il.indexjson = strings.Replace(il.indexjson, "", strconv.FormatInt(desc.Size, 10), -1) + + // create index blob file + desc, err = createIndexFile(il.rootDir, il.index) + if err != nil { + return err + } + il.indexjson = strings.Replace(il.indexjson, "", string(desc.Digest), -1) + il.indexjson = strings.Replace(il.indexjson, "", strconv.FormatInt(desc.Size, 10), -1) - return createIndexFile(il.rootDir, desc) + // create index.json file + return createIndexJSON(il.rootDir, il.indexjson) } func createLayoutFile(root string) error { @@ -241,19 +280,34 @@ func createLayoutFile(root string) error { return err } -func createIndexFile(root string, mft v1.Descriptor) error { +func createIndexJSON(root string, str string) error { indexpath := filepath.Join(root, "index.json") f, err := os.Create(indexpath) if err != nil { return err } defer f.Close() - indexStr = strings.Replace(indexStr, "", string(mft.Digest), -1) - indexStr = strings.Replace(indexStr, "", strconv.FormatInt(mft.Size, 10), -1) - _, err = io.Copy(f, bytes.NewBuffer([]byte(indexStr))) + _, err = io.Copy(f, bytes.NewBuffer([]byte(str))) + return err } +func createIndexFile(root, str string) (v1.Descriptor, error) { + name := filepath.Join(root, "blobs", "sha256", "test-index") + f, err := os.Create(name) + if err != nil { + return v1.Descriptor{}, err + } + defer f.Close() + + _, err = io.Copy(f, bytes.NewBuffer([]byte(str))) + if err != nil { + return v1.Descriptor{}, err + } + + return createHashedBlob(name) +} + func createManifestFile(root, str string) (v1.Descriptor, error) { name := filepath.Join(root, "blobs", "sha256", "test-manifest") f, err := os.Create(name) diff --git a/image/index.go b/image/index.go new file mode 100644 index 0000000..f0c8d61 --- /dev/null +++ b/image/index.go @@ -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 +} diff --git a/man/oci-image-tool-create.1.md b/man/oci-image-tool-create.1.md index 869a41f..2e9ce6e 100644 --- a/man/oci-image-tool-create.1.md +++ b/man/oci-image-tool-create.1.md @@ -26,6 +26,11 @@ runtime-spec-compatible `dest/config.json`. **--type**="" Type of the file to unpack. If unset, oci-image-tool will try to auto-detect the type. One of "imageLayout,image" +**--platform**="" + Specify the os and architecture of the manifest, format is OS:Architecture. + e.g. --platform linux:amd64 + Only applicable if reftype is index. + # EXAMPLES ``` $ skopeo copy docker://busybox oci:busybox-oci diff --git a/man/oci-image-tool-unpack.1.md b/man/oci-image-tool-unpack.1.md index 9b2bccb..95d3aec 100644 --- a/man/oci-image-tool-unpack.1.md +++ b/man/oci-image-tool-unpack.1.md @@ -20,6 +20,11 @@ oci-image-tool unpack \- Unpack an image or image source layout **--type**="" Type of the file to unpack. If unset, oci-image-tool will try to auto-detect the type. One of "imageLayout,image" +**--platform**="" + Specify the os and architecture of the manifest, format is OS:Architecture. + e.g. --platform linux:amd64 + Only applicable if reftype is index. + # EXAMPLES ``` $ skopeo copy docker://busybox oci:busybox-oci