moby/daemon/volumes.go
Brian Goff e744b0dcba Fix volume ref restore process
Fixes #9629 #9768

A couple of issues:

1) Volume config is not restored if we couldn't find it with the graph
driver, but bind-mounts would never be found by the graph driver since
they aren't in that dir

2) container volumes were only being restored if they were found in the
volumes repo, but volumes created by old daemons wouldn't be in the
repo until the container is at least started.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
2015-01-20 15:54:56 -05:00

393 lines
10 KiB
Go

package daemon
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/execdriver"
"github.com/docker/docker/pkg/chrootarchive"
"github.com/docker/docker/pkg/symlink"
"github.com/docker/docker/volumes"
"github.com/docker/libcontainer/label"
)
type Mount struct {
MountToPath string
container *Container
volume *volumes.Volume
Writable bool
copyData bool
from *Container
}
func (mnt *Mount) Export(resource string) (io.ReadCloser, error) {
var name string
if resource == mnt.MountToPath[1:] {
name = filepath.Base(resource)
}
path, err := filepath.Rel(mnt.MountToPath[1:], resource)
if err != nil {
return nil, err
}
return mnt.volume.Export(path, name)
}
func (container *Container) prepareVolumes() error {
if container.Volumes == nil || len(container.Volumes) == 0 {
container.Volumes = make(map[string]string)
container.VolumesRW = make(map[string]bool)
}
return container.createVolumes()
}
// sortedVolumeMounts returns the list of container volume mount points sorted in lexicographic order
func (container *Container) sortedVolumeMounts() []string {
var mountPaths []string
for path := range container.Volumes {
mountPaths = append(mountPaths, path)
}
sort.Strings(mountPaths)
return mountPaths
}
func (container *Container) createVolumes() error {
mounts, err := container.parseVolumeMountConfig()
if err != nil {
return err
}
for _, mnt := range mounts {
if err := mnt.initialize(); err != nil {
return err
}
}
// On every start, this will apply any new `VolumesFrom` entries passed in via HostConfig, which may override volumes set in `create`
return container.applyVolumesFrom()
}
func (m *Mount) initialize() error {
// No need to initialize anything since it's already been initialized
if hostPath, exists := m.container.Volumes[m.MountToPath]; exists {
// If this is a bind-mount/volumes-from, maybe it was passed in at start instead of create
// We need to make sure bind-mounts/volumes-from passed on start can override existing ones.
if !m.volume.IsBindMount && m.from == nil {
return nil
}
if m.volume.Path == hostPath {
return nil
}
// Make sure we remove these old volumes we don't actually want now.
// Ignore any errors here since this is just cleanup, maybe someone volumes-from'd this volume
v := m.container.daemon.volumes.Get(hostPath)
v.RemoveContainer(m.container.ID)
m.container.daemon.volumes.Delete(v.Path)
}
// This is the full path to container fs + mntToPath
containerMntPath, err := symlink.FollowSymlinkInScope(filepath.Join(m.container.basefs, m.MountToPath), m.container.basefs)
if err != nil {
return err
}
m.container.VolumesRW[m.MountToPath] = m.Writable
m.container.Volumes[m.MountToPath] = m.volume.Path
m.volume.AddContainer(m.container.ID)
if m.Writable && m.copyData {
// Copy whatever is in the container at the mntToPath to the volume
copyExistingContents(containerMntPath, m.volume.Path)
}
return nil
}
func (container *Container) VolumePaths() map[string]struct{} {
var paths = make(map[string]struct{})
for _, path := range container.Volumes {
paths[path] = struct{}{}
}
return paths
}
func (container *Container) registerVolumes() {
for path := range container.VolumePaths() {
if v := container.daemon.volumes.Get(path); v != nil {
v.AddContainer(container.ID)
continue
}
// if container was created with an old daemon, this volume may not be registered so we need to make sure it gets registered
writable := true
if rw, exists := container.VolumesRW[path]; exists {
writable = rw
}
v, err := container.daemon.volumes.FindOrCreateVolume(path, writable)
if err != nil {
log.Debugf("error registering volume %s: %v", path, err)
continue
}
v.AddContainer(container.ID)
}
}
func (container *Container) derefVolumes() {
for path := range container.VolumePaths() {
vol := container.daemon.volumes.Get(path)
if vol == nil {
log.Debugf("Volume %s was not found and could not be dereferenced", path)
continue
}
vol.RemoveContainer(container.ID)
}
}
func (container *Container) parseVolumeMountConfig() (map[string]*Mount, error) {
var mounts = make(map[string]*Mount)
// Get all the bind mounts
for _, spec := range container.hostConfig.Binds {
path, mountToPath, writable, err := parseBindMountSpec(spec)
if err != nil {
return nil, err
}
// Check if a volume already exists for this and use it
vol, err := container.daemon.volumes.FindOrCreateVolume(path, writable)
if err != nil {
return nil, err
}
mounts[mountToPath] = &Mount{
container: container,
volume: vol,
MountToPath: mountToPath,
Writable: writable,
}
}
// Get the rest of the volumes
for path := range container.Config.Volumes {
// Check if this is already added as a bind-mount
path = filepath.Clean(path)
if _, exists := mounts[path]; exists {
continue
}
// Check if this has already been created
if _, exists := container.Volumes[path]; exists {
continue
}
vol, err := container.daemon.volumes.FindOrCreateVolume("", true)
if err != nil {
return nil, err
}
mounts[path] = &Mount{
container: container,
MountToPath: path,
volume: vol,
Writable: true,
copyData: true,
}
}
return mounts, nil
}
func parseBindMountSpec(spec string) (string, string, bool, error) {
var (
path, mountToPath string
writable bool
arr = strings.Split(spec, ":")
)
switch len(arr) {
case 2:
path = arr[0]
mountToPath = arr[1]
writable = true
case 3:
path = arr[0]
mountToPath = arr[1]
writable = validMountMode(arr[2]) && arr[2] == "rw"
default:
return "", "", false, fmt.Errorf("Invalid volume specification: %s", spec)
}
if !filepath.IsAbs(path) {
return "", "", false, fmt.Errorf("cannot bind mount volume: %s volume paths must be absolute.", path)
}
path = filepath.Clean(path)
mountToPath = filepath.Clean(mountToPath)
return path, mountToPath, writable, nil
}
func parseVolumesFromSpec(spec string) (string, string, error) {
specParts := strings.SplitN(spec, ":", 2)
if len(specParts) == 0 {
return "", "", fmt.Errorf("malformed volumes-from specification: %s", spec)
}
var (
id = specParts[0]
mode = "rw"
)
if len(specParts) == 2 {
mode = specParts[1]
if !validMountMode(mode) {
return "", "", fmt.Errorf("invalid mode for volumes-from: %s", mode)
}
}
return id, mode, nil
}
func (container *Container) applyVolumesFrom() error {
volumesFrom := container.hostConfig.VolumesFrom
if len(volumesFrom) > 0 && container.AppliedVolumesFrom == nil {
container.AppliedVolumesFrom = make(map[string]struct{})
}
mountGroups := make(map[string][]*Mount)
for _, spec := range volumesFrom {
id, mode, err := parseVolumesFromSpec(spec)
if err != nil {
return err
}
if _, exists := container.AppliedVolumesFrom[id]; exists {
// Don't try to apply these since they've already been applied
continue
}
c := container.daemon.Get(id)
if c == nil {
return fmt.Errorf("container %s not found, impossible to mount its volumes", id)
}
var (
fromMounts = c.VolumeMounts()
mounts []*Mount
)
for _, mnt := range fromMounts {
mnt.Writable = mnt.Writable && (mode == "rw")
mounts = append(mounts, mnt)
}
mountGroups[id] = mounts
}
for id, mounts := range mountGroups {
for _, mnt := range mounts {
mnt.from = mnt.container
mnt.container = container
if err := mnt.initialize(); err != nil {
return err
}
}
container.AppliedVolumesFrom[id] = struct{}{}
}
return nil
}
func validMountMode(mode string) bool {
validModes := map[string]bool{
"rw": true,
"ro": true,
}
return validModes[mode]
}
func (container *Container) setupMounts() error {
mounts := []execdriver.Mount{
{Source: container.ResolvConfPath, Destination: "/etc/resolv.conf", Writable: true, Private: true},
}
if container.HostnamePath != "" {
mounts = append(mounts, execdriver.Mount{Source: container.HostnamePath, Destination: "/etc/hostname", Writable: true, Private: true})
}
if container.HostsPath != "" {
mounts = append(mounts, execdriver.Mount{Source: container.HostsPath, Destination: "/etc/hosts", Writable: true, Private: true})
}
for _, m := range mounts {
if err := label.SetFileLabel(m.Source, container.MountLabel); err != nil {
return err
}
}
// Mount user specified volumes
// Note, these are not private because you may want propagation of (un)mounts from host
// volumes. For instance if you use -v /usr:/usr and the host later mounts /usr/share you
// want this new mount in the container
// These mounts must be ordered based on the length of the path that it is being mounted to (lexicographic)
for _, path := range container.sortedVolumeMounts() {
mounts = append(mounts, execdriver.Mount{
Source: container.Volumes[path],
Destination: path,
Writable: container.VolumesRW[path],
})
}
container.command.Mounts = mounts
return nil
}
func (container *Container) VolumeMounts() map[string]*Mount {
mounts := make(map[string]*Mount)
for mountToPath, path := range container.Volumes {
if v := container.daemon.volumes.Get(path); v != nil {
mounts[mountToPath] = &Mount{volume: v, container: container, MountToPath: mountToPath, Writable: container.VolumesRW[mountToPath]}
}
}
return mounts
}
func copyExistingContents(source, destination string) error {
volList, err := ioutil.ReadDir(source)
if err != nil {
return err
}
if len(volList) > 0 {
srcList, err := ioutil.ReadDir(destination)
if err != nil {
return err
}
if len(srcList) == 0 {
// If the source volume is empty copy files from the root into the volume
if err := chrootarchive.CopyWithTar(source, destination); err != nil {
return err
}
}
}
return copyOwnership(source, destination)
}
// copyOwnership copies the permissions and uid:gid of the source file
// into the destination file
func copyOwnership(source, destination string) error {
var stat syscall.Stat_t
if err := syscall.Stat(source, &stat); err != nil {
return err
}
if err := os.Chown(destination, int(stat.Uid), int(stat.Gid)); err != nil {
return err
}
return os.Chmod(destination, os.FileMode(stat.Mode))
}