Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions cmd/oci-image-tool/create_runtime_bundle.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions cmd/oci-image-tool/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/oci-image-tool/unpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
156 changes: 156 additions & 0 deletions image/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// 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{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're currently not translating this to anything (because the runtime spec doesn't have knobs for Linux networking). @philips, can you explain how you expect higher-levels notice and apply this configuration?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But i'm sure that other extentsions will come along and use such. Also, having a golang structure for the data will allow folks to import and use it for their own use cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Thu, Jun 02, 2016 at 11:49:50AM -0700, Vincent Batts wrote:

  • ExposedPorts map[string]struct{}

But i'm sure that other extentsions will come along and use such.

How do they find it? If this command unpacks $IMAGE into $BUNDLE, but
does not leave a pointer in $BUNDLE to say “this was unpacked from
$IMAGE”, I don't see how tooling invoked on the $BUNDLE would be able
to find this image-specific data. So to support that additional
tooling, you either need:

a. A generic “this was unpacked from $IMAGE” reference in the bundle
directory somewhere, or
b. To copy all of the “unused” information like ExposedPorts over to
some well-known location inside the bundle.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vbatts the cfg structure currently is private; here in image-spec we don't have redundantly declared public Golang structs, only the JSON schema files. If we want to go the route as https://github.com/opencontainers/runtime-spec/tree/master/specs-go, I'd have to refactor; please advise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wking commented on #87 about networking.

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) {
if c.OS != "linux" {
return nil, fmt.Errorf("%s: unsupported OS", c.OS)
}

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")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should probably be throwing “not supported yet” errors if it can't make the conversion until you add code to spin up a container with all the mounts and use getent or similar (see opencontainers/runtime-spec#38). Docker may be doing something like this already, so maybe you can grab their code. And this is hideous, ugly, and dangerous, which is why the runtime spec punted it to config authors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack, I'll return an error in this case, thanks for the suggestion 👍


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

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
}
65 changes: 65 additions & 0 deletions image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package image

import (
"encoding/json"
"os"
"path/filepath"

"github.com/pkg/errors"
)
Expand Down Expand Up @@ -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)
}