diff --git a/docs/yaml.md b/docs/yaml.md index 094fb38f1..1bbe72451 100644 --- a/docs/yaml.md +++ b/docs/yaml.md @@ -183,6 +183,8 @@ permissions issues in use. In addition to the parts of the specification above used to generate the OCI spec, there is a `runtime` section in the image specification which specifies some actions to take place when the container is being started. +- `mounts` takes a list of mount specifications (`source`, `destination`, `type`, `options`) and mounts them in the root namespace before the container is created. It will + try to make any missing destination directories. - `mkdir` takes a list of directories to create at runtime, in the root mount namespace. These are created before the container is started, so they can be used to create directories for bind mounts, for example in `/tmp` or `/run` which would otherwise be empty. - `interface` defines a list of actions to perform on a network interface: diff --git a/src/moby/build.go b/src/moby/build.go index f98d01e75..33a7a2988 100644 --- a/src/moby/build.go +++ b/src/moby/build.go @@ -120,7 +120,7 @@ func enforceContentTrust(fullImageName string, config *TrustConfig) bool { return false } -func outputImage(image Image, section string, prefix string, m Moby, idMap map[string]uint32, pull bool, iw *tar.Writer) error { +func outputImage(image Image, section string, prefix string, m Moby, idMap map[string]uint32, dupMap map[string]string, pull bool, iw *tar.Writer) error { log.Infof(" Create OCI config for %s", image.Image) useTrust := enforceContentTrust(image.Image, &m.Trust) oci, runtime, err := ConfigToOCI(image, useTrust, idMap) @@ -131,13 +131,9 @@ func outputImage(image Image, section string, prefix string, m Moby, idMap map[s if err != nil { return fmt.Errorf("Failed to create config for %s: %v", image.Image, err) } - runtimeConfig, err := json.MarshalIndent(runtime, "", " ") - if err != nil { - return fmt.Errorf("Failed to create runtime config for %s: %v", image.Image, err) - } path := path.Join("containers", section, prefix+image.Name) readonly := oci.Root.Readonly - err = ImageBundle(path, image.Image, config, runtimeConfig, iw, useTrust, pull, readonly) + err = ImageBundle(path, image.Image, config, runtime, iw, useTrust, pull, readonly, dupMap) if err != nil { return fmt.Errorf("Failed to extract root filesystem for %s: %v", image.Image, err) } @@ -171,6 +167,9 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error { id++ } + // deduplicate containers with the same image + dupMap := map[string]string{} + if m.Kernel.Image != "" { // get kernel and initrd tarball from container log.Infof("Extract kernel image: %s", m.Kernel.Image) @@ -202,7 +201,7 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error { } for i, image := range m.Onboot { so := fmt.Sprintf("%03d", i) - if err := outputImage(image, "onboot", so+"-", m, idMap, pull, iw); err != nil { + if err := outputImage(image, "onboot", so+"-", m, idMap, dupMap, pull, iw); err != nil { return err } } @@ -212,7 +211,7 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error { } for i, image := range m.Onshutdown { so := fmt.Sprintf("%03d", i) - if err := outputImage(image, "onshutdown", so+"-", m, idMap, pull, iw); err != nil { + if err := outputImage(image, "onshutdown", so+"-", m, idMap, dupMap, pull, iw); err != nil { return err } } @@ -221,7 +220,7 @@ func Build(m Moby, w io.Writer, pull bool, tp string) error { log.Infof("Add service containers:") } for _, image := range m.Services { - if err := outputImage(image, "services", "", m, idMap, pull, iw); err != nil { + if err := outputImage(image, "services", "", m, idMap, dupMap, pull, iw); err != nil { return err } } diff --git a/src/moby/config.go b/src/moby/config.go index 7945966f4..7eed658f5 100644 --- a/src/moby/config.go +++ b/src/moby/config.go @@ -93,9 +93,10 @@ type Image struct { // Runtime is the type of config processed at runtime, not used to build the OCI spec type Runtime struct { - Mkdir []string `yaml:"mkdir" json:"mkdir,omitempty"` - Interfaces []Interface `yaml:"interfaces" json:"interfaces,omitempty"` - BindNS *Namespaces `yaml:"bindNS" json:"bindNS,omitempty"` + Mounts []specs.Mount `yaml:"mounts" json:"mounts,omitempty"` + Mkdir []string `yaml:"mkdir" json:"mkdir,omitempty"` + Interfaces []Interface `yaml:"interfaces" json:"interfaces,omitempty"` + BindNS Namespaces `yaml:"bindNS" json:"bindNS,omitempty"` } // Namespaces is the type for configuring paths to bind namespaces @@ -727,7 +728,6 @@ func ConfigInspectToOCI(yaml Image, inspect types.ImageInspect, idMap map[string sort.Sort(mountList) namespaces := []specs.LinuxNamespace{} - // to attach to an existing namespace, easiest to bind mount with nsfs in a system container // net, ipc, and uts namespaces: default to not creating a new namespace (usually host namespace) netNS := assignStringEmpty3("root", label.Net, yaml.Net) diff --git a/src/moby/image.go b/src/moby/image.go index ce5706c8e..d6af09d69 100644 --- a/src/moby/image.go +++ b/src/moby/image.go @@ -3,6 +3,7 @@ package moby import ( "archive/tar" "bytes" + "encoding/json" "fmt" "io" "io/ioutil" @@ -10,6 +11,7 @@ import ( "strings" log "github.com/Sirupsen/logrus" + "github.com/opencontainers/runtime-spec/specs-go" ) type tarWriter interface { @@ -189,18 +191,28 @@ func ImageTar(image, prefix string, tw tarWriter, trust bool, pull bool, resolv } // ImageBundle produces an OCI bundle at the given path in a tarball, given an image and a config.json -func ImageBundle(prefix string, image string, config []byte, runtimeConfig []byte, tw tarWriter, trust bool, pull bool, readonly bool) error { - log.Debugf("image bundle: %s %s cfg: %s runtime: %s", prefix, image, string(config), string(runtimeConfig)) - +func ImageBundle(prefix string, image string, config []byte, runtime Runtime, tw tarWriter, trust bool, pull bool, readonly bool, dupMap map[string]string) error { // if read only, just unpack in rootfs/ but otherwise set up for overlay - rootfs := "rootfs" + rootExtract := "rootfs" if !readonly { - rootfs = "lower" + rootExtract = "lower" } - if err := ImageTar(image, path.Join(prefix, rootfs)+"/", tw, trust, pull, ""); err != nil { - return err + // See if we have extracted this image previously + root := path.Join(prefix, rootExtract) + var foundElsewhere = dupMap[image] != "" + if !foundElsewhere { + if err := ImageTar(image, root+"/", tw, trust, pull, ""); err != nil { + return err + } + dupMap[image] = root + } else { + if err := tarPrefix(prefix+"/", tw); err != nil { + return err + } + root = dupMap[image] } + hdr := &tar.Header{ Name: path.Join(prefix, "config.json"), Mode: 0644, @@ -214,26 +226,11 @@ func ImageBundle(prefix string, image string, config []byte, runtimeConfig []byt return err } - // do not write an empty runtime config - if string(runtimeConfig) != "{}" { - hdr = &tar.Header{ - Name: path.Join(prefix, "runtime.json"), - Mode: 0644, - Size: int64(len(runtimeConfig)), - } - if err := tw.WriteHeader(hdr); err != nil { - return err - } - buf = bytes.NewBuffer(runtimeConfig) - if _, err := io.Copy(tw, buf); err != nil { - return err - } - } - if !readonly { // add a tmp directory to be used as a mount point for tmpfs for upper, work + tmp := path.Join(prefix, "tmp") hdr = &tar.Header{ - Name: path.Join(prefix, "tmp"), + Name: tmp, Mode: 0755, Typeflag: tar.TypeDir, } @@ -249,7 +246,47 @@ func ImageBundle(prefix string, image string, config []byte, runtimeConfig []byt if err := tw.WriteHeader(hdr); err != nil { return err } + runtime.Mounts = append(runtime.Mounts, specs.Mount{Source: "tmpfs", Type: "tmpfs", Destination: "/" + tmp}) + // remount private as nothing else should see the temporary layers + runtime.Mounts = append(runtime.Mounts, specs.Mount{Destination: "/" + tmp, Options: []string{"remount", "private"}}) + overlayOptions := []string{"lowerdir=/" + root, "upperdir=/" + path.Join(tmp, "upper"), "workdir=/" + path.Join(tmp, "work")} + runtime.Mounts = append(runtime.Mounts, specs.Mount{Source: "overlay", Type: "overlay", Destination: "/" + path.Join(prefix, "rootfs"), Options: overlayOptions}) + } else { + if foundElsewhere { + // we need to make the mountpoint at rootfs + hdr = &tar.Header{ + Name: path.Join(prefix, "rootfs"), + Mode: 0755, + Typeflag: tar.TypeDir, + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + } + // either bind from another location, or bind from self to make sure it is a mountpoint as runc prefers this + runtime.Mounts = append(runtime.Mounts, specs.Mount{Source: "/" + root, Destination: "/" + path.Join(prefix, "rootfs"), Options: []string{"bind"}}) } + // write the runtime config + runtimeConfig, err := json.MarshalIndent(runtime, "", " ") + if err != nil { + return fmt.Errorf("Failed to create runtime config for %s: %v", image, err) + } + + hdr = &tar.Header{ + Name: path.Join(prefix, "runtime.json"), + Mode: 0644, + Size: int64(len(runtimeConfig)), + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + buf = bytes.NewBuffer(runtimeConfig) + if _, err := io.Copy(tw, buf); err != nil { + return err + } + + log.Debugf("image bundle: %s %s cfg: %s runtime: %s", prefix, image, string(config), string(runtimeConfig)) + return nil } diff --git a/src/moby/schema.go b/src/moby/schema.go index 2fd65e838..6aa71b617 100644 --- a/src/moby/schema.go +++ b/src/moby/schema.go @@ -239,6 +239,7 @@ var schema = string(` "type": "object", "additionalProperties": false, "properties": { + "mounts": {"$ref": "#/definitions/mounts"}, "mkdir": {"$ref": "#/definitions/strings"}, "interfaces": {"$ref": "#/definitions/interfaces"}, "bindNS": {"$ref": "#/definitions/namespaces"}