Files
linuxkit/src/moby/image.go
Justin Cormack cfa5d273b7 De-dup container root filesystems
With the mount framework we can de-dup containers that share the same image.

Signed-off-by: Justin Cormack <justin.cormack@docker.com>
2017-08-22 17:27:08 +01:00

293 lines
8.1 KiB
Go

package moby
import (
"archive/tar"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"path"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/opencontainers/runtime-spec/specs-go"
)
type tarWriter interface {
Close() error
Flush() error
Write(b []byte) (n int, err error)
WriteHeader(hdr *tar.Header) error
}
// This uses Docker to convert a Docker image into a tarball. It would be an improvement if we
// used the containerd libraries to do this instead locally direct from a local image
// cache as it would be much simpler.
// Unfortunately there are some files that Docker always makes appear in a running image and
// export shows them. In particular we have no way for a user to specify their own resolv.conf.
// Even if we were not using docker export to get the image, users of docker build cannot override
// the resolv.conf either, as it is not writeable and bind mounted in.
var exclude = map[string]bool{
".dockerenv": true,
"Dockerfile": true,
"dev/console": true,
"dev/pts": true,
"dev/shm": true,
"etc/hostname": true,
}
var replace = map[string]string{
"etc/hosts": `127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
`,
"etc/resolv.conf": `
# no resolv.conf configured
`,
}
// tarPrefix creates the leading directories for a path
func tarPrefix(path string, tw tarWriter) error {
if path == "" {
return nil
}
if path[len(path)-1] != byte('/') {
return fmt.Errorf("path does not end with /: %s", path)
}
path = path[:len(path)-1]
if path[0] == byte('/') {
return fmt.Errorf("path should be relative: %s", path)
}
mkdir := ""
for _, dir := range strings.Split(path, "/") {
mkdir = mkdir + dir
hdr := &tar.Header{
Name: mkdir,
Mode: 0755,
Typeflag: tar.TypeDir,
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
mkdir = mkdir + "/"
}
return nil
}
// ImageTar takes a Docker image and outputs it to a tar stream
func ImageTar(image, prefix string, tw tarWriter, trust bool, pull bool, resolv string) error {
log.Debugf("image tar: %s %s", image, prefix)
if prefix != "" && prefix[len(prefix)-1] != byte('/') {
return fmt.Errorf("prefix does not end with /: %s", prefix)
}
err := tarPrefix(prefix, tw)
if err != nil {
return err
}
if pull || trust {
err := dockerPull(image, pull, trust)
if err != nil {
return fmt.Errorf("Could not pull image %s: %v", image, err)
}
}
container, err := dockerCreate(image)
if err != nil {
// if the image wasn't found, pull it down. Bail on other errors.
if strings.Contains(err.Error(), "No such image") {
err := dockerPull(image, true, trust)
if err != nil {
return fmt.Errorf("Could not pull image %s: %v", image, err)
}
container, err = dockerCreate(image)
if err != nil {
return fmt.Errorf("Failed to docker create image %s: %v", image, err)
}
} else {
return fmt.Errorf("Failed to create docker image %s: %v", image, err)
}
}
contents, err := dockerExport(container)
if err != nil {
return fmt.Errorf("Failed to docker export container from container %s: %v", container, err)
}
err = dockerRm(container)
if err != nil {
return fmt.Errorf("Failed to docker rm container %s: %v", container, err)
}
// now we need to filter out some files from the resulting tar archive
r := bytes.NewReader(contents)
tr := tar.NewReader(r)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if exclude[hdr.Name] {
log.Debugf("image tar: %s %s exclude %s", image, prefix, hdr.Name)
_, err = io.Copy(ioutil.Discard, tr)
if err != nil {
return err
}
} else if replace[hdr.Name] != "" {
if hdr.Name != "etc/resolv.conf" || resolv == "" {
contents := replace[hdr.Name]
hdr.Size = int64(len(contents))
hdr.Name = prefix + hdr.Name
log.Debugf("image tar: %s %s add %s", image, prefix, hdr.Name)
if err := tw.WriteHeader(hdr); err != nil {
return err
}
buf := bytes.NewBufferString(contents)
_, err = io.Copy(tw, buf)
if err != nil {
return err
}
} else {
// replace resolv.conf with specified symlink
hdr.Name = prefix + hdr.Name
hdr.Size = 0
hdr.Typeflag = tar.TypeSymlink
hdr.Linkname = resolv
log.Debugf("image tar: %s %s add resolv symlink /etc/resolv.conf -> %s", image, prefix, resolv)
if err := tw.WriteHeader(hdr); err != nil {
return err
}
}
_, err = io.Copy(ioutil.Discard, tr)
if err != nil {
return err
}
} else {
log.Debugf("image tar: %s %s add %s", image, prefix, hdr.Name)
hdr.Name = prefix + hdr.Name
if hdr.Typeflag == tar.TypeLink {
// hard links are referenced by full path so need to be adjusted
hdr.Linkname = prefix + hdr.Linkname
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
_, err = io.Copy(tw, tr)
if err != nil {
return err
}
}
}
return nil
}
// 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, dupMap map[string]string) error {
// if read only, just unpack in rootfs/ but otherwise set up for overlay
rootExtract := "rootfs"
if !readonly {
rootExtract = "lower"
}
// 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,
Size: int64(len(config)),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
buf := bytes.NewBuffer(config)
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: tmp,
Mode: 0755,
Typeflag: tar.TypeDir,
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
// add rootfs as merged mount point
hdr = &tar.Header{
Name: path.Join(prefix, "rootfs"),
Mode: 0755,
Typeflag: tar.TypeDir,
}
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
}