fb62e18441
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>
346 lines
8.6 KiB
Go
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))
|
|
}
|