moby/daemon/volumes.go
Thomas Orozco fb62e18441 Fix: Failed Start breaks VolumesFrom
Running parseVolumesFromSpec on all VolumesFrom specs before initialize
any mounts endures that we don't leave container.Volumes in an
inconsistent (partially initialized) if one of out mount groups is not
available (e.g. the container we're trying to mount from does not
exist).

Keeping container.Volumes in a consistent state ensures that next time
we Start() the container, it'll run prepareVolumes() again.

The attached test demonstrates that when a container fails to start due
to a missing container specified in VolumesFrom, it "remembers" a Volume
that worked.

Fixes: #8726

Signed-off-by: Thomas Orozco <thomas@orozco.fr>
2014-11-10 17:32:16 +01:00

346 lines
8.6 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/archive"
"github.com/docker/docker/pkg/symlink"
"github.com/docker/docker/volumes"
)
type Mount struct {
MountToPath string
container *Container
volume *volumes.Volume
Writable bool
copyData bool
}
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)
if err := container.applyVolumesFrom(); err != nil {
return err
}
}
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
}
}
return nil
}
func (m *Mount) initialize() error {
// No need to initialize anything since it's already been initialized
if _, exists := m.container.Volumes[m.MountToPath]; exists {
return nil
}
// 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 _, mnt := range container.VolumeMounts() {
mnt.volume.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 (container *Container) applyVolumesFrom() error {
volumesFrom := container.hostConfig.VolumesFrom
mountGroups := make([]map[string]*Mount, 0, len(volumesFrom))
for _, spec := range volumesFrom {
mountGroup, err := parseVolumesFromSpec(container.daemon, spec)
if err != nil {
return err
}
mountGroups = append(mountGroups, mountGroup)
}
for _, mounts := range mountGroups {
for _, mnt := range mounts {
mnt.container = container
if err := mnt.initialize(); err != nil {
return err
}
}
}
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})
}
// 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 parseVolumesFromSpec(daemon *Daemon, spec string) (map[string]*Mount, error) {
specParts := strings.SplitN(spec, ":", 2)
if len(specParts) == 0 {
return nil, fmt.Errorf("Malformed volumes-from specification: %s", spec)
}
c := daemon.Get(specParts[0])
if c == nil {
return nil, fmt.Errorf("Container %s not found. Impossible to mount its volumes", specParts[0])
}
mounts := c.VolumeMounts()
if len(specParts) == 2 {
mode := specParts[1]
if !validMountMode(mode) {
return nil, fmt.Errorf("Invalid mode for volumes-from: %s", mode)
}
// Set the mode for the inheritted volume
for _, mnt := range mounts {
// Ensure that if the inherited volume is not writable, that we don't make
// it writable here
mnt.Writable = mnt.Writable && (mode == "rw")
}
}
return mounts, 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 := archive.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))
}