From 70a918d049d88c4c73cd67f751daf062fd4fa9f7 Mon Sep 17 00:00:00 2001 From: Sergiusz Urbaniak Date: Thu, 2 Jun 2016 16:03:42 +0200 Subject: [PATCH] oci-image-tool: implement create-runtime-bundle Fixes #99, #87 Signed-off-by: Sergiusz Urbaniak --- cmd/oci-image-tool/create_runtime_bundle.go | 109 ++++++++++++++ cmd/oci-image-tool/main.go | 1 + cmd/oci-image-tool/unpack.go | 2 +- image/config.go | 154 ++++++++++++++++++++ image/image.go | 65 +++++++++ 5 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 cmd/oci-image-tool/create_runtime_bundle.go create mode 100644 image/config.go diff --git a/cmd/oci-image-tool/create_runtime_bundle.go b/cmd/oci-image-tool/create_runtime_bundle.go new file mode 100644 index 000000000..1ec8ab167 --- /dev/null +++ b/cmd/oci-image-tool/create_runtime_bundle.go @@ -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 main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/opencontainers/image-spec/image" + "github.com/spf13/cobra" +) + +// supported bundle types +var bundleTypes = []string{ + typeImageLayout, + typeImage, +} + +type bundleCmd struct { + stdout *log.Logger + stderr *log.Logger + typ string // the type to bundle, can be empty string + ref string + root string +} + +func newBundleCmd(stdout, stderr *log.Logger) *cobra.Command { + v := &bundleCmd{ + stdout: stdout, + stderr: stderr, + } + + cmd := &cobra.Command{ + Use: "create-runtime-bundle [src] [dest]", + Short: "Create an OCI image runtime bundle", + Long: `Creates an OCI image runtime bundle at the destination directory [dest] from an OCI image present at [src].`, + Run: v.Run, + } + + cmd.Flags().StringVar( + &v.typ, "type", "", + fmt.Sprintf( + `Type of the file to unpack. If unset, oci-image-tool will try to auto-detect the type. One of "%s"`, + strings.Join(bundleTypes, ","), + ), + ) + + cmd.Flags().StringVar( + &v.ref, "ref", "v1.0", + `The ref pointing to the manifest of the OCI image. This must be present in the "refs" subdirectory of the image.`, + ) + + cmd.Flags().StringVar( + &v.root, "rootfs", "rootfs", + `A directory representing the root filesystem of the container in the OCI runtime bundle. +It is strongly recommended to keep the default value.`, + ) + + return cmd +} + +func (v *bundleCmd) Run(cmd *cobra.Command, args []string) { + if len(args) != 2 { + v.stderr.Print("both src and dest must be provided") + if err := cmd.Usage(); err != nil { + v.stderr.Println(err) + } + os.Exit(1) + } + + if v.typ == "" { + typ, err := autodetect(args[0]) + if err != nil { + v.stderr.Printf("%q: autodetection failed: %v", args[0], err) + os.Exit(1) + } + v.typ = typ + } + + var err error + switch v.typ { + case typeImageLayout: + err = image.CreateRuntimeBundleLayout(args[0], args[1], v.ref, v.root) + + case typeImage: + err = image.CreateRuntimeBundle(args[0], args[1], v.ref, v.root) + } + + if err != nil { + v.stderr.Printf("unpacking failed: %v", err) + os.Exit(1) + } + + os.Exit(0) +} diff --git a/cmd/oci-image-tool/main.go b/cmd/oci-image-tool/main.go index ab911bb39..7cd350e88 100644 --- a/cmd/oci-image-tool/main.go +++ b/cmd/oci-image-tool/main.go @@ -32,6 +32,7 @@ func main() { cmd.AddCommand(newValidateCmd(stdout, stderr)) cmd.AddCommand(newUnpackCmd(stdout, stderr)) + cmd.AddCommand(newBundleCmd(stdout, stderr)) if err := cmd.Execute(); err != nil { stderr.Println(err) diff --git a/cmd/oci-image-tool/unpack.go b/cmd/oci-image-tool/unpack.go index ee9e67ad8..e02ad706d 100644 --- a/cmd/oci-image-tool/unpack.go +++ b/cmd/oci-image-tool/unpack.go @@ -33,7 +33,7 @@ var unpackTypes = []string{ type unpackCmd struct { stdout *log.Logger stderr *log.Logger - typ string // the type to validate, can be empty string + typ string // the type to unpack, can be empty string ref string } diff --git a/image/config.go b/image/config.go new file mode 100644 index 000000000..c1a919c14 --- /dev/null +++ b/image/config.go @@ -0,0 +1,154 @@ +// 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/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +type cfg struct { + User string + Memory int64 + MemorySwap int64 + CPUShares int64 `json:"CpuShares"` + ExposedPorts map[string]struct{} + Env []string + Entrypoint []string + Cmd []string + Volumes map[string]struct{} + WorkingDir string +} + +type config struct { + Architecture string `json:"architecture"` + OS string `json:"os"` + Config cfg `json:"config"` +} + +func findConfig(w walker, d *descriptor) (*config, error) { + var c config + cpath := filepath.Join("blobs", d.Digest) + + f := func(path string, info os.FileInfo, r io.Reader) error { + if info.IsDir() { + return nil + } + + if 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.MediaTypeImageSerializationConfig.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 + } + + switch err := w.walk(f); err { + case nil: + return nil, fmt.Errorf("%s: config not found", cpath) + case errEOW: + // found, continue below + default: + return nil, err + } + + return &c, nil +} + +func (c *config) runtimeSpec(rootfs string) (*specs.Spec, error) { + var s specs.Spec + s.Version = "0.5.0" + s.Root.Path = rootfs + s.Process.Cwd = c.Config.WorkingDir + s.Process.Env = append([]string(nil), c.Config.Env...) + s.Process.Args = append([]string(nil), c.Config.Entrypoint...) + s.Process.Args = append(s.Process.Args, c.Config.Cmd...) + + 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 { + return nil, errors.New("config.User: unsupported format") + } + + s.Platform.OS = c.OS + s.Platform.Arch = c.Architecture + + if c.OS == "linux" { + mem := uint64(c.Config.Memory) + swap := uint64(c.Config.MemorySwap) + shares := uint64(c.Config.CPUShares) + + s.Linux.Resources = &specs.Resources{ + CPU: &specs.CPU{ + Shares: &shares, + }, + + Memory: &specs.Memory{ + Limit: &mem, + Reservation: &mem, + Swap: &swap, + }, + } + } + + for vol := range c.Config.Volumes { + s.Mounts = append( + s.Mounts, + specs.Mount{ + Destination: vol, + Type: "bind", + Options: []string{"rbind"}, + }, + ) + } + + return &s, nil +} diff --git a/image/image.go b/image/image.go index 8805f590e..ffc2ca18d 100644 --- a/image/image.go +++ b/image/image.go @@ -15,7 +15,9 @@ package image import ( + "encoding/json" "os" + "path/filepath" "github.com/pkg/errors" ) @@ -101,3 +103,66 @@ func unpack(w walker, dest, refName string) error { return m.unpack(w, dest) } + +// CreateRuntimeBundleLayout walks through the file tree given 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) +} + +// 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 { + 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) +} + +func createRuntimeBundle(w walker, dest, refName, rootfs string) error { + ref, err := findDescriptor(w, refName) + if err != nil { + return err + } + + if err = ref.validate(w); err != nil { + return err + } + + m, err := findManifest(w, ref) + if err != nil { + return err + } + + if err = m.validate(w); err != nil { + return err + } + + c, err := findConfig(w, &m.Config) + if err != nil { + return err + } + + err = m.unpack(w, filepath.Join(dest, rootfs)) + if 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) +}