diff --git a/src/moby/build.go b/src/moby/build.go index 9ce628bd3..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) @@ -133,7 +133,7 @@ func outputImage(image Image, section string, prefix string, m Moby, idMap map[s } path := path.Join("containers", section, prefix+image.Name) readonly := oci.Root.Readonly - err = ImageBundle(path, image.Image, config, runtime, 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) } @@ -167,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) @@ -198,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 } } @@ -208,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 } } @@ -217,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/image.go b/src/moby/image.go index 4d7fb3e8c..d6af09d69 100644 --- a/src/moby/image.go +++ b/src/moby/image.go @@ -191,16 +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, runtime Runtime, tw tarWriter, trust bool, pull bool, readonly bool) error { +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, @@ -237,11 +249,22 @@ func ImageBundle(prefix string, image string, config []byte, runtime Runtime, tw 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=/" + path.Join(prefix, "lower"), "upperdir=/" + path.Join(tmp, "upper"), "workdir=/" + path.Join(tmp, "work")} + 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 { - // make rootfs a mountpoint as runc can be picky about this - runtime.Mounts = append(runtime.Mounts, specs.Mount{Source: path.Join(prefix, rootfs), Destination: "/" + path.Join(prefix, "rootfs"), Options: []string{"bind"}}) + 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