Signed-off-by: John Howard <jhoward@microsoft.com>
| ... | ... |
@@ -331,7 +331,12 @@ func (s *router) postContainersCreate(ctx context.Context, w http.ResponseWriter |
| 331 | 331 |
version := httputils.VersionFromContext(ctx) |
| 332 | 332 |
adjustCPUShares := version.LessThan("1.19")
|
| 333 | 333 |
|
| 334 |
- ccr, err := s.daemon.ContainerCreate(name, config, hostConfig, adjustCPUShares) |
|
| 334 |
+ ccr, err := s.daemon.ContainerCreate(&daemon.ContainerCreateConfig{
|
|
| 335 |
+ Name: name, |
|
| 336 |
+ Config: config, |
|
| 337 |
+ HostConfig: hostConfig, |
|
| 338 |
+ AdjustCPUShares: adjustCPUShares, |
|
| 339 |
+ }) |
|
| 335 | 340 |
if err != nil {
|
| 336 | 341 |
return err |
| 337 | 342 |
} |
| ... | ... |
@@ -186,7 +186,7 @@ func platformSupports(command string) error {
|
| 186 | 186 |
return nil |
| 187 | 187 |
} |
| 188 | 188 |
switch command {
|
| 189 |
- case "expose", "volume", "user", "stopsignal", "arg": |
|
| 189 |
+ case "expose", "user", "stopsignal", "arg": |
|
| 190 | 190 |
return fmt.Errorf("The daemon on this platform does not support the command '%s'", command)
|
| 191 | 191 |
} |
| 192 | 192 |
return nil |
| ... | ... |
@@ -8,7 +8,7 @@ package daemon |
| 8 | 8 |
func checkIfPathIsInAVolume(container *Container, absPath string) (bool, error) {
|
| 9 | 9 |
var toVolume bool |
| 10 | 10 |
for _, mnt := range container.MountPoints {
|
| 11 |
- if toVolume = mnt.hasResource(absPath); toVolume {
|
|
| 11 |
+ if toVolume = mnt.HasResource(absPath); toVolume {
|
|
| 12 | 12 |
if mnt.RW {
|
| 13 | 13 |
break |
| 14 | 14 |
} |
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"io/ioutil" |
| 9 | 9 |
"os" |
| 10 | 10 |
"path/filepath" |
| 11 |
+ "strings" |
|
| 11 | 12 |
"sync" |
| 12 | 13 |
"syscall" |
| 13 | 14 |
"time" |
| ... | ... |
@@ -30,8 +31,10 @@ import ( |
| 30 | 30 |
"github.com/docker/docker/pkg/promise" |
| 31 | 31 |
"github.com/docker/docker/pkg/signal" |
| 32 | 32 |
"github.com/docker/docker/pkg/symlink" |
| 33 |
+ "github.com/docker/docker/pkg/system" |
|
| 33 | 34 |
"github.com/docker/docker/runconfig" |
| 34 | 35 |
"github.com/docker/docker/volume" |
| 36 |
+ "github.com/docker/docker/volume/store" |
|
| 35 | 37 |
) |
| 36 | 38 |
|
| 37 | 39 |
var ( |
| ... | ... |
@@ -72,6 +75,7 @@ type CommonContainer struct {
|
| 72 | 72 |
RestartCount int |
| 73 | 73 |
HasBeenStartedBefore bool |
| 74 | 74 |
HasBeenManuallyStopped bool // used for unless-stopped restart policy |
| 75 |
+ MountPoints map[string]*volume.MountPoint |
|
| 75 | 76 |
hostConfig *runconfig.HostConfig |
| 76 | 77 |
command *execdriver.Command |
| 77 | 78 |
monitor *containerMonitor |
| ... | ... |
@@ -1108,29 +1112,109 @@ func (container *Container) mountVolumes() error {
|
| 1108 | 1108 |
return nil |
| 1109 | 1109 |
} |
| 1110 | 1110 |
|
| 1111 |
-func (container *Container) copyImagePathContent(v volume.Volume, destination string) error {
|
|
| 1112 |
- rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs) |
|
| 1113 |
- if err != nil {
|
|
| 1114 |
- return err |
|
| 1111 |
+func (container *Container) prepareMountPoints() error {
|
|
| 1112 |
+ for _, config := range container.MountPoints {
|
|
| 1113 |
+ if len(config.Driver) > 0 {
|
|
| 1114 |
+ v, err := container.daemon.createVolume(config.Name, config.Driver, nil) |
|
| 1115 |
+ if err != nil {
|
|
| 1116 |
+ return err |
|
| 1117 |
+ } |
|
| 1118 |
+ config.Volume = v |
|
| 1119 |
+ } |
|
| 1115 | 1120 |
} |
| 1121 |
+ return nil |
|
| 1122 |
+} |
|
| 1116 | 1123 |
|
| 1117 |
- if _, err = ioutil.ReadDir(rootfs); err != nil {
|
|
| 1118 |
- if os.IsNotExist(err) {
|
|
| 1119 |
- return nil |
|
| 1124 |
+func (container *Container) removeMountPoints(rm bool) error {
|
|
| 1125 |
+ var rmErrors []string |
|
| 1126 |
+ for _, m := range container.MountPoints {
|
|
| 1127 |
+ if m.Volume == nil {
|
|
| 1128 |
+ continue |
|
| 1129 |
+ } |
|
| 1130 |
+ container.daemon.volumes.Decrement(m.Volume) |
|
| 1131 |
+ if rm {
|
|
| 1132 |
+ err := container.daemon.volumes.Remove(m.Volume) |
|
| 1133 |
+ // ErrVolumeInUse is ignored because having this |
|
| 1134 |
+ // volume being referenced by other container is |
|
| 1135 |
+ // not an error, but an implementation detail. |
|
| 1136 |
+ // This prevents docker from logging "ERROR: Volume in use" |
|
| 1137 |
+ // where there is another container using the volume. |
|
| 1138 |
+ if err != nil && err != store.ErrVolumeInUse {
|
|
| 1139 |
+ rmErrors = append(rmErrors, err.Error()) |
|
| 1140 |
+ } |
|
| 1120 | 1141 |
} |
| 1121 |
- return err |
|
| 1122 | 1142 |
} |
| 1143 |
+ if len(rmErrors) > 0 {
|
|
| 1144 |
+ return derr.ErrorCodeRemovingVolume.WithArgs(strings.Join(rmErrors, "\n")) |
|
| 1145 |
+ } |
|
| 1146 |
+ return nil |
|
| 1147 |
+} |
|
| 1123 | 1148 |
|
| 1124 |
- path, err := v.Mount() |
|
| 1125 |
- if err != nil {
|
|
| 1126 |
- return err |
|
| 1149 |
+func (container *Container) unmountVolumes(forceSyscall bool) error {
|
|
| 1150 |
+ var ( |
|
| 1151 |
+ volumeMounts []volume.MountPoint |
|
| 1152 |
+ err error |
|
| 1153 |
+ ) |
|
| 1154 |
+ |
|
| 1155 |
+ for _, mntPoint := range container.MountPoints {
|
|
| 1156 |
+ dest, err := container.GetResourcePath(mntPoint.Destination) |
|
| 1157 |
+ if err != nil {
|
|
| 1158 |
+ return err |
|
| 1159 |
+ } |
|
| 1160 |
+ |
|
| 1161 |
+ volumeMounts = append(volumeMounts, volume.MountPoint{Destination: dest, Volume: mntPoint.Volume})
|
|
| 1127 | 1162 |
} |
| 1128 | 1163 |
|
| 1129 |
- if err := copyExistingContents(rootfs, path); err != nil {
|
|
| 1164 |
+ // Append any network mounts to the list (this is a no-op on Windows) |
|
| 1165 |
+ if volumeMounts, err = appendNetworkMounts(container, volumeMounts); err != nil {
|
|
| 1130 | 1166 |
return err |
| 1131 | 1167 |
} |
| 1132 | 1168 |
|
| 1133 |
- return v.Unmount() |
|
| 1169 |
+ for _, volumeMount := range volumeMounts {
|
|
| 1170 |
+ if forceSyscall {
|
|
| 1171 |
+ system.UnmountWithSyscall(volumeMount.Destination) |
|
| 1172 |
+ } |
|
| 1173 |
+ |
|
| 1174 |
+ if volumeMount.Volume != nil {
|
|
| 1175 |
+ if err := volumeMount.Volume.Unmount(); err != nil {
|
|
| 1176 |
+ return err |
|
| 1177 |
+ } |
|
| 1178 |
+ } |
|
| 1179 |
+ } |
|
| 1180 |
+ |
|
| 1181 |
+ return nil |
|
| 1182 |
+} |
|
| 1183 |
+ |
|
| 1184 |
+func (container *Container) addBindMountPoint(name, source, destination string, rw bool) {
|
|
| 1185 |
+ container.MountPoints[destination] = &volume.MountPoint{
|
|
| 1186 |
+ Name: name, |
|
| 1187 |
+ Source: source, |
|
| 1188 |
+ Destination: destination, |
|
| 1189 |
+ RW: rw, |
|
| 1190 |
+ } |
|
| 1191 |
+} |
|
| 1192 |
+ |
|
| 1193 |
+func (container *Container) addLocalMountPoint(name, destination string, rw bool) {
|
|
| 1194 |
+ container.MountPoints[destination] = &volume.MountPoint{
|
|
| 1195 |
+ Name: name, |
|
| 1196 |
+ Driver: volume.DefaultDriverName, |
|
| 1197 |
+ Destination: destination, |
|
| 1198 |
+ RW: rw, |
|
| 1199 |
+ } |
|
| 1200 |
+} |
|
| 1201 |
+ |
|
| 1202 |
+func (container *Container) addMountPointWithVolume(destination string, vol volume.Volume, rw bool) {
|
|
| 1203 |
+ container.MountPoints[destination] = &volume.MountPoint{
|
|
| 1204 |
+ Name: vol.Name(), |
|
| 1205 |
+ Driver: vol.DriverName(), |
|
| 1206 |
+ Destination: destination, |
|
| 1207 |
+ RW: rw, |
|
| 1208 |
+ Volume: vol, |
|
| 1209 |
+ } |
|
| 1210 |
+} |
|
| 1211 |
+ |
|
| 1212 |
+func (container *Container) isDestinationMounted(destination string) bool {
|
|
| 1213 |
+ return container.MountPoints[destination] != nil |
|
| 1134 | 1214 |
} |
| 1135 | 1215 |
|
| 1136 | 1216 |
func (container *Container) stopSignal() int {
|
| ... | ... |
@@ -23,12 +23,12 @@ import ( |
| 23 | 23 |
"github.com/docker/docker/pkg/idtools" |
| 24 | 24 |
"github.com/docker/docker/pkg/nat" |
| 25 | 25 |
"github.com/docker/docker/pkg/stringid" |
| 26 |
+ "github.com/docker/docker/pkg/symlink" |
|
| 26 | 27 |
"github.com/docker/docker/pkg/system" |
| 27 | 28 |
"github.com/docker/docker/pkg/ulimit" |
| 28 | 29 |
"github.com/docker/docker/runconfig" |
| 29 | 30 |
"github.com/docker/docker/utils" |
| 30 | 31 |
"github.com/docker/docker/volume" |
| 31 |
- "github.com/docker/docker/volume/store" |
|
| 32 | 32 |
"github.com/docker/libnetwork" |
| 33 | 33 |
"github.com/docker/libnetwork/drivers/bridge" |
| 34 | 34 |
"github.com/docker/libnetwork/netlabel" |
| ... | ... |
@@ -54,9 +54,8 @@ type Container struct {
|
| 54 | 54 |
AppArmorProfile string |
| 55 | 55 |
HostnamePath string |
| 56 | 56 |
HostsPath string |
| 57 |
- ShmPath string |
|
| 58 |
- MqueuePath string |
|
| 59 |
- MountPoints map[string]*mountPoint |
|
| 57 |
+ ShmPath string // TODO Windows - Factor this out (GH15862) |
|
| 58 |
+ MqueuePath string // TODO Windows - Factor this out (GH15862) |
|
| 60 | 59 |
ResolvConfPath string |
| 61 | 60 |
|
| 62 | 61 |
Volumes map[string]string // Deprecated since 1.7, kept for backwards compatibility |
| ... | ... |
@@ -1197,40 +1196,16 @@ func (container *Container) disconnectFromNetwork(n libnetwork.Network) error {
|
| 1197 | 1197 |
return nil |
| 1198 | 1198 |
} |
| 1199 | 1199 |
|
| 1200 |
-func (container *Container) unmountVolumes(forceSyscall bool) error {
|
|
| 1201 |
- var volumeMounts []mountPoint |
|
| 1202 |
- |
|
| 1203 |
- for _, mntPoint := range container.MountPoints {
|
|
| 1204 |
- dest, err := container.GetResourcePath(mntPoint.Destination) |
|
| 1205 |
- if err != nil {
|
|
| 1206 |
- return err |
|
| 1207 |
- } |
|
| 1208 |
- |
|
| 1209 |
- volumeMounts = append(volumeMounts, mountPoint{Destination: dest, Volume: mntPoint.Volume})
|
|
| 1210 |
- } |
|
| 1211 |
- |
|
| 1200 |
+// appendNetworkMounts appends any network mounts to the array of mount points passed in |
|
| 1201 |
+func appendNetworkMounts(container *Container, volumeMounts []volume.MountPoint) ([]volume.MountPoint, error) {
|
|
| 1212 | 1202 |
for _, mnt := range container.networkMounts() {
|
| 1213 | 1203 |
dest, err := container.GetResourcePath(mnt.Destination) |
| 1214 | 1204 |
if err != nil {
|
| 1215 |
- return err |
|
| 1216 |
- } |
|
| 1217 |
- |
|
| 1218 |
- volumeMounts = append(volumeMounts, mountPoint{Destination: dest})
|
|
| 1219 |
- } |
|
| 1220 |
- |
|
| 1221 |
- for _, volumeMount := range volumeMounts {
|
|
| 1222 |
- if forceSyscall {
|
|
| 1223 |
- syscall.Unmount(volumeMount.Destination, 0) |
|
| 1224 |
- } |
|
| 1225 |
- |
|
| 1226 |
- if volumeMount.Volume != nil {
|
|
| 1227 |
- if err := volumeMount.Volume.Unmount(); err != nil {
|
|
| 1228 |
- return err |
|
| 1229 |
- } |
|
| 1205 |
+ return nil, err |
|
| 1230 | 1206 |
} |
| 1207 |
+ volumeMounts = append(volumeMounts, volume.MountPoint{Destination: dest})
|
|
| 1231 | 1208 |
} |
| 1232 |
- |
|
| 1233 |
- return nil |
|
| 1209 |
+ return volumeMounts, nil |
|
| 1234 | 1210 |
} |
| 1235 | 1211 |
|
| 1236 | 1212 |
func (container *Container) networkMounts() []execdriver.Mount {
|
| ... | ... |
@@ -1290,74 +1265,29 @@ func (container *Container) networkMounts() []execdriver.Mount {
|
| 1290 | 1290 |
return mounts |
| 1291 | 1291 |
} |
| 1292 | 1292 |
|
| 1293 |
-func (container *Container) addBindMountPoint(name, source, destination string, rw bool) {
|
|
| 1294 |
- container.MountPoints[destination] = &mountPoint{
|
|
| 1295 |
- Name: name, |
|
| 1296 |
- Source: source, |
|
| 1297 |
- Destination: destination, |
|
| 1298 |
- RW: rw, |
|
| 1293 |
+func (container *Container) copyImagePathContent(v volume.Volume, destination string) error {
|
|
| 1294 |
+ rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs) |
|
| 1295 |
+ if err != nil {
|
|
| 1296 |
+ return err |
|
| 1299 | 1297 |
} |
| 1300 |
-} |
|
| 1301 | 1298 |
|
| 1302 |
-func (container *Container) addLocalMountPoint(name, destination string, rw bool) {
|
|
| 1303 |
- container.MountPoints[destination] = &mountPoint{
|
|
| 1304 |
- Name: name, |
|
| 1305 |
- Driver: volume.DefaultDriverName, |
|
| 1306 |
- Destination: destination, |
|
| 1307 |
- RW: rw, |
|
| 1299 |
+ if _, err = ioutil.ReadDir(rootfs); err != nil {
|
|
| 1300 |
+ if os.IsNotExist(err) {
|
|
| 1301 |
+ return nil |
|
| 1302 |
+ } |
|
| 1303 |
+ return err |
|
| 1308 | 1304 |
} |
| 1309 |
-} |
|
| 1310 | 1305 |
|
| 1311 |
-func (container *Container) addMountPointWithVolume(destination string, vol volume.Volume, rw bool) {
|
|
| 1312 |
- container.MountPoints[destination] = &mountPoint{
|
|
| 1313 |
- Name: vol.Name(), |
|
| 1314 |
- Driver: vol.DriverName(), |
|
| 1315 |
- Destination: destination, |
|
| 1316 |
- RW: rw, |
|
| 1317 |
- Volume: vol, |
|
| 1306 |
+ path, err := v.Mount() |
|
| 1307 |
+ if err != nil {
|
|
| 1308 |
+ return err |
|
| 1318 | 1309 |
} |
| 1319 |
-} |
|
| 1320 | 1310 |
|
| 1321 |
-func (container *Container) isDestinationMounted(destination string) bool {
|
|
| 1322 |
- return container.MountPoints[destination] != nil |
|
| 1323 |
-} |
|
| 1324 |
- |
|
| 1325 |
-func (container *Container) prepareMountPoints() error {
|
|
| 1326 |
- for _, config := range container.MountPoints {
|
|
| 1327 |
- if len(config.Driver) > 0 {
|
|
| 1328 |
- v, err := container.daemon.createVolume(config.Name, config.Driver, nil) |
|
| 1329 |
- if err != nil {
|
|
| 1330 |
- return err |
|
| 1331 |
- } |
|
| 1332 |
- config.Volume = v |
|
| 1333 |
- } |
|
| 1311 |
+ if err := copyExistingContents(rootfs, path); err != nil {
|
|
| 1312 |
+ return err |
|
| 1334 | 1313 |
} |
| 1335 |
- return nil |
|
| 1336 |
-} |
|
| 1337 | 1314 |
|
| 1338 |
-func (container *Container) removeMountPoints(rm bool) error {
|
|
| 1339 |
- var rmErrors []string |
|
| 1340 |
- for _, m := range container.MountPoints {
|
|
| 1341 |
- if m.Volume == nil {
|
|
| 1342 |
- continue |
|
| 1343 |
- } |
|
| 1344 |
- container.daemon.volumes.Decrement(m.Volume) |
|
| 1345 |
- if rm {
|
|
| 1346 |
- err := container.daemon.volumes.Remove(m.Volume) |
|
| 1347 |
- // ErrVolumeInUse is ignored because having this |
|
| 1348 |
- // volume being referenced by othe container is |
|
| 1349 |
- // not an error, but an implementation detail. |
|
| 1350 |
- // This prevents docker from logging "ERROR: Volume in use" |
|
| 1351 |
- // where there is another container using the volume. |
|
| 1352 |
- if err != nil && err != store.ErrVolumeInUse {
|
|
| 1353 |
- rmErrors = append(rmErrors, err.Error()) |
|
| 1354 |
- } |
|
| 1355 |
- } |
|
| 1356 |
- } |
|
| 1357 |
- if len(rmErrors) > 0 {
|
|
| 1358 |
- return derr.ErrorCodeRemovingVolume.WithArgs(strings.Join(rmErrors, "\n")) |
|
| 1359 |
- } |
|
| 1360 |
- return nil |
|
| 1315 |
+ return v.Unmount() |
|
| 1361 | 1316 |
} |
| 1362 | 1317 |
|
| 1363 | 1318 |
func (container *Container) shmPath() (string, error) {
|
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/daemon/execdriver" |
| 9 | 9 |
derr "github.com/docker/docker/errors" |
| 10 |
+ "github.com/docker/docker/volume" |
|
| 10 | 11 |
"github.com/docker/libnetwork" |
| 11 | 12 |
) |
| 12 | 13 |
|
| ... | ... |
@@ -169,18 +170,11 @@ func (container *Container) updateNetwork() error {
|
| 169 | 169 |
func (container *Container) releaseNetwork() {
|
| 170 | 170 |
} |
| 171 | 171 |
|
| 172 |
-func (container *Container) unmountVolumes(forceSyscall bool) error {
|
|
| 173 |
- return nil |
|
| 174 |
-} |
|
| 175 |
- |
|
| 176 |
-// prepareMountPoints is a no-op on Windows |
|
| 177 |
-func (container *Container) prepareMountPoints() error {
|
|
| 178 |
- return nil |
|
| 179 |
-} |
|
| 180 |
- |
|
| 181 |
-// removeMountPoints is a no-op on Windows. |
|
| 182 |
-func (container *Container) removeMountPoints(_ bool) error {
|
|
| 183 |
- return nil |
|
| 172 |
+// appendNetworkMounts appends any network mounts to the array of mount points passed in. |
|
| 173 |
+// Windows does not support network mounts (not to be confused with SMB network mounts), so |
|
| 174 |
+// this is a no-op. |
|
| 175 |
+func appendNetworkMounts(container *Container, volumeMounts []volume.MountPoint) ([]volume.MountPoint, error) {
|
|
| 176 |
+ return volumeMounts, nil |
|
| 184 | 177 |
} |
| 185 | 178 |
|
| 186 | 179 |
func (container *Container) setupIpcDirs() error {
|
| ... | ... |
@@ -15,26 +15,34 @@ import ( |
| 15 | 15 |
"github.com/opencontainers/runc/libcontainer/label" |
| 16 | 16 |
) |
| 17 | 17 |
|
| 18 |
+// ContainerCreateConfig is the parameter set to ContainerCreate() |
|
| 19 |
+type ContainerCreateConfig struct {
|
|
| 20 |
+ Name string |
|
| 21 |
+ Config *runconfig.Config |
|
| 22 |
+ HostConfig *runconfig.HostConfig |
|
| 23 |
+ AdjustCPUShares bool |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 18 | 26 |
// ContainerCreate takes configs and creates a container. |
| 19 |
-func (daemon *Daemon) ContainerCreate(name string, config *runconfig.Config, hostConfig *runconfig.HostConfig, adjustCPUShares bool) (types.ContainerCreateResponse, error) {
|
|
| 20 |
- if config == nil {
|
|
| 27 |
+func (daemon *Daemon) ContainerCreate(params *ContainerCreateConfig) (types.ContainerCreateResponse, error) {
|
|
| 28 |
+ if params.Config == nil {
|
|
| 21 | 29 |
return types.ContainerCreateResponse{}, derr.ErrorCodeEmptyConfig
|
| 22 | 30 |
} |
| 23 | 31 |
|
| 24 |
- warnings, err := daemon.verifyContainerSettings(hostConfig, config) |
|
| 32 |
+ warnings, err := daemon.verifyContainerSettings(params.HostConfig, params.Config) |
|
| 25 | 33 |
if err != nil {
|
| 26 | 34 |
return types.ContainerCreateResponse{"", warnings}, err
|
| 27 | 35 |
} |
| 28 | 36 |
|
| 29 |
- daemon.adaptContainerSettings(hostConfig, adjustCPUShares) |
|
| 37 |
+ daemon.adaptContainerSettings(params.HostConfig, params.AdjustCPUShares) |
|
| 30 | 38 |
|
| 31 |
- container, err := daemon.Create(config, hostConfig, name) |
|
| 39 |
+ container, err := daemon.create(params) |
|
| 32 | 40 |
if err != nil {
|
| 33 |
- if daemon.Graph().IsNotExist(err, config.Image) {
|
|
| 34 |
- if strings.Contains(config.Image, "@") {
|
|
| 35 |
- return types.ContainerCreateResponse{"", warnings}, derr.ErrorCodeNoSuchImageHash.WithArgs(config.Image)
|
|
| 41 |
+ if daemon.Graph().IsNotExist(err, params.Config.Image) {
|
|
| 42 |
+ if strings.Contains(params.Config.Image, "@") {
|
|
| 43 |
+ return types.ContainerCreateResponse{"", warnings}, derr.ErrorCodeNoSuchImageHash.WithArgs(params.Config.Image)
|
|
| 36 | 44 |
} |
| 37 |
- img, tag := parsers.ParseRepositoryTag(config.Image) |
|
| 45 |
+ img, tag := parsers.ParseRepositoryTag(params.Config.Image) |
|
| 38 | 46 |
if tag == "" {
|
| 39 | 47 |
tag = tags.DefaultTag |
| 40 | 48 |
} |
| ... | ... |
@@ -47,7 +55,7 @@ func (daemon *Daemon) ContainerCreate(name string, config *runconfig.Config, hos |
| 47 | 47 |
} |
| 48 | 48 |
|
| 49 | 49 |
// Create creates a new container from the given configuration with a given name. |
| 50 |
-func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.HostConfig, name string) (retC *Container, retErr error) {
|
|
| 50 |
+func (daemon *Daemon) create(params *ContainerCreateConfig) (retC *Container, retErr error) {
|
|
| 51 | 51 |
var ( |
| 52 | 52 |
container *Container |
| 53 | 53 |
img *image.Image |
| ... | ... |
@@ -55,8 +63,8 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos |
| 55 | 55 |
err error |
| 56 | 56 |
) |
| 57 | 57 |
|
| 58 |
- if config.Image != "" {
|
|
| 59 |
- img, err = daemon.repositories.LookupImage(config.Image) |
|
| 58 |
+ if params.Config.Image != "" {
|
|
| 59 |
+ img, err = daemon.repositories.LookupImage(params.Config.Image) |
|
| 60 | 60 |
if err != nil {
|
| 61 | 61 |
return nil, err |
| 62 | 62 |
} |
| ... | ... |
@@ -66,20 +74,20 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos |
| 66 | 66 |
imgID = img.ID |
| 67 | 67 |
} |
| 68 | 68 |
|
| 69 |
- if err := daemon.mergeAndVerifyConfig(config, img); err != nil {
|
|
| 69 |
+ if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil {
|
|
| 70 | 70 |
return nil, err |
| 71 | 71 |
} |
| 72 | 72 |
|
| 73 |
- if hostConfig == nil {
|
|
| 74 |
- hostConfig = &runconfig.HostConfig{}
|
|
| 73 |
+ if params.HostConfig == nil {
|
|
| 74 |
+ params.HostConfig = &runconfig.HostConfig{}
|
|
| 75 | 75 |
} |
| 76 |
- if hostConfig.SecurityOpt == nil {
|
|
| 77 |
- hostConfig.SecurityOpt, err = daemon.generateSecurityOpt(hostConfig.IpcMode, hostConfig.PidMode) |
|
| 76 |
+ if params.HostConfig.SecurityOpt == nil {
|
|
| 77 |
+ params.HostConfig.SecurityOpt, err = daemon.generateSecurityOpt(params.HostConfig.IpcMode, params.HostConfig.PidMode) |
|
| 78 | 78 |
if err != nil {
|
| 79 | 79 |
return nil, err |
| 80 | 80 |
} |
| 81 | 81 |
} |
| 82 |
- if container, err = daemon.newContainer(name, config, imgID); err != nil {
|
|
| 82 |
+ if container, err = daemon.newContainer(params.Name, params.Config, imgID); err != nil {
|
|
| 83 | 83 |
return nil, err |
| 84 | 84 |
} |
| 85 | 85 |
defer func() {
|
| ... | ... |
@@ -96,7 +104,7 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos |
| 96 | 96 |
if err := daemon.createRootfs(container); err != nil {
|
| 97 | 97 |
return nil, err |
| 98 | 98 |
} |
| 99 |
- if err := daemon.setHostConfig(container, hostConfig); err != nil {
|
|
| 99 |
+ if err := daemon.setHostConfig(container, params.HostConfig); err != nil {
|
|
| 100 | 100 |
return nil, err |
| 101 | 101 |
} |
| 102 | 102 |
defer func() {
|
| ... | ... |
@@ -111,7 +119,7 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos |
| 111 | 111 |
} |
| 112 | 112 |
defer container.Unmount() |
| 113 | 113 |
|
| 114 |
- if err := createContainerPlatformSpecificSettings(container, config, hostConfig, img); err != nil {
|
|
| 114 |
+ if err := createContainerPlatformSpecificSettings(container, params.Config, params.HostConfig, img); err != nil {
|
|
| 115 | 115 |
return nil, err |
| 116 | 116 |
} |
| 117 | 117 |
|
| ... | ... |
@@ -16,9 +16,11 @@ import ( |
| 16 | 16 |
|
| 17 | 17 |
// createContainerPlatformSpecificSettings performs platform specific container create functionality |
| 18 | 18 |
func createContainerPlatformSpecificSettings(container *Container, config *runconfig.Config, hostConfig *runconfig.HostConfig, img *image.Image) error {
|
| 19 |
+ var name, destination string |
|
| 20 |
+ |
|
| 19 | 21 |
for spec := range config.Volumes {
|
| 20 |
- name := stringid.GenerateNonCryptoID() |
|
| 21 |
- destination := filepath.Clean(spec) |
|
| 22 |
+ name = stringid.GenerateNonCryptoID() |
|
| 23 |
+ destination = filepath.Clean(spec) |
|
| 22 | 24 |
|
| 23 | 25 |
// Skip volumes for which we already have something mounted on that |
| 24 | 26 |
// destination because of a --volume-from. |
| ... | ... |
@@ -1,11 +1,83 @@ |
| 1 | 1 |
package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "fmt" |
|
| 5 |
+ |
|
| 4 | 6 |
"github.com/docker/docker/image" |
| 7 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 5 | 8 |
"github.com/docker/docker/runconfig" |
| 9 |
+ "github.com/docker/docker/volume" |
|
| 6 | 10 |
) |
| 7 | 11 |
|
| 8 | 12 |
// createContainerPlatformSpecificSettings performs platform specific container create functionality |
| 9 | 13 |
func createContainerPlatformSpecificSettings(container *Container, config *runconfig.Config, hostConfig *runconfig.HostConfig, img *image.Image) error {
|
| 14 |
+ for spec := range config.Volumes {
|
|
| 15 |
+ |
|
| 16 |
+ mp, err := volume.ParseMountSpec(spec, hostConfig.VolumeDriver) |
|
| 17 |
+ if err != nil {
|
|
| 18 |
+ return fmt.Errorf("Unrecognised volume spec: %v", err)
|
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ // If the mountpoint doesn't have a name, generate one. |
|
| 22 |
+ if len(mp.Name) == 0 {
|
|
| 23 |
+ mp.Name = stringid.GenerateNonCryptoID() |
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ // Skip volumes for which we already have something mounted on that |
|
| 27 |
+ // destination because of a --volume-from. |
|
| 28 |
+ if container.isDestinationMounted(mp.Destination) {
|
|
| 29 |
+ continue |
|
| 30 |
+ } |
|
| 31 |
+ |
|
| 32 |
+ volumeDriver := hostConfig.VolumeDriver |
|
| 33 |
+ if mp.Destination != "" && img != nil {
|
|
| 34 |
+ if _, ok := img.ContainerConfig.Volumes[mp.Destination]; ok {
|
|
| 35 |
+ // check for whether bind is not specified and then set to local |
|
| 36 |
+ if _, ok := container.MountPoints[mp.Destination]; !ok {
|
|
| 37 |
+ volumeDriver = volume.DefaultDriverName |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ // Create the volume in the volume driver. If it doesn't exist, |
|
| 43 |
+ // a new one will be created. |
|
| 44 |
+ v, err := container.daemon.createVolume(mp.Name, volumeDriver, nil) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return err |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ // FIXME Windows: This code block is present in the Linux version and |
|
| 50 |
+ // allows the contents to be copied to the container FS prior to it |
|
| 51 |
+ // being started. However, the function utilises the FollowSymLinkInScope |
|
| 52 |
+ // path which does not cope with Windows volume-style file paths. There |
|
| 53 |
+ // is a seperate effort to resolve this (@swernli), so this processing |
|
| 54 |
+ // is deferred for now. A case where this would be useful is when |
|
| 55 |
+ // a dockerfile includes a VOLUME statement, but something is created |
|
| 56 |
+ // in that directory during the dockerfile processing. What this means |
|
| 57 |
+ // on Windows for TP4 is that in that scenario, the contents will not |
|
| 58 |
+ // copied, but that's (somewhat) OK as HCS will bomb out soon after |
|
| 59 |
+ // at it doesn't support mapped directories which have contents in the |
|
| 60 |
+ // destination path anyway. |
|
| 61 |
+ // |
|
| 62 |
+ // Example for repro later: |
|
| 63 |
+ // FROM windowsservercore |
|
| 64 |
+ // RUN mkdir c:\myvol |
|
| 65 |
+ // RUN copy c:\windows\system32\ntdll.dll c:\myvol |
|
| 66 |
+ // VOLUME "c:\myvol" |
|
| 67 |
+ // |
|
| 68 |
+ // Then |
|
| 69 |
+ // docker build -t vol . |
|
| 70 |
+ // docker run -it --rm vol cmd <-- This is where HCS will error out. |
|
| 71 |
+ // |
|
| 72 |
+ // // never attempt to copy existing content in a container FS to a shared volume |
|
| 73 |
+ // if v.DriverName() == volume.DefaultDriverName {
|
|
| 74 |
+ // if err := container.copyImagePathContent(v, mp.Destination); err != nil {
|
|
| 75 |
+ // return err |
|
| 76 |
+ // } |
|
| 77 |
+ // } |
|
| 78 |
+ |
|
| 79 |
+ // Add it to container.MountPoints |
|
| 80 |
+ container.addMountPointWithVolume(mp.Destination, v, mp.RW) |
|
| 81 |
+ } |
|
| 10 | 82 |
return nil |
| 11 | 83 |
} |
| ... | ... |
@@ -22,6 +22,7 @@ import ( |
| 22 | 22 |
"github.com/docker/docker/pkg/sysinfo" |
| 23 | 23 |
"github.com/docker/docker/runconfig" |
| 24 | 24 |
"github.com/docker/docker/utils" |
| 25 |
+ "github.com/docker/docker/volume" |
|
| 25 | 26 |
"github.com/docker/libnetwork" |
| 26 | 27 |
nwconfig "github.com/docker/libnetwork/config" |
| 27 | 28 |
"github.com/docker/libnetwork/drivers/bridge" |
| ... | ... |
@@ -603,10 +604,10 @@ func (daemon *Daemon) newBaseContainer(id string) Container {
|
| 603 | 603 |
State: NewState(), |
| 604 | 604 |
execCommands: newExecStore(), |
| 605 | 605 |
root: daemon.containerRoot(id), |
| 606 |
+ MountPoints: make(map[string]*volume.MountPoint), |
|
| 606 | 607 |
}, |
| 607 |
- MountPoints: make(map[string]*mountPoint), |
|
| 608 |
- Volumes: make(map[string]string), |
|
| 609 |
- VolumesRW: make(map[string]bool), |
|
| 608 |
+ Volumes: make(map[string]string), |
|
| 609 |
+ VolumesRW: make(map[string]bool), |
|
| 610 | 610 |
} |
| 611 | 611 |
} |
| 612 | 612 |
|
| ... | ... |
@@ -83,7 +83,12 @@ func (d Docker) Container(id string) (*daemon.Container, error) {
|
| 83 | 83 |
|
| 84 | 84 |
// Create creates a new Docker container and returns potential warnings |
| 85 | 85 |
func (d Docker) Create(cfg *runconfig.Config, hostCfg *runconfig.HostConfig) (*daemon.Container, []string, error) {
|
| 86 |
- ccr, err := d.Daemon.ContainerCreate("", cfg, hostCfg, true)
|
|
| 86 |
+ ccr, err := d.Daemon.ContainerCreate(&daemon.ContainerCreateConfig{
|
|
| 87 |
+ Name: "", |
|
| 88 |
+ Config: cfg, |
|
| 89 |
+ HostConfig: hostCfg, |
|
| 90 |
+ AdjustCPUShares: true, |
|
| 91 |
+ }) |
|
| 87 | 92 |
if err != nil {
|
| 88 | 93 |
return nil, nil, err |
| 89 | 94 |
} |
| ... | ... |
@@ -165,16 +165,8 @@ type ResourceStats struct {
|
| 165 | 165 |
SystemUsage uint64 `json:"system_usage"` |
| 166 | 166 |
} |
| 167 | 167 |
|
| 168 |
-// Mount contains information for a mount operation. |
|
| 169 |
-type Mount struct {
|
|
| 170 |
- Source string `json:"source"` |
|
| 171 |
- Destination string `json:"destination"` |
|
| 172 |
- Writable bool `json:"writable"` |
|
| 173 |
- Private bool `json:"private"` |
|
| 174 |
- Slave bool `json:"slave"` |
|
| 175 |
-} |
|
| 176 |
- |
|
| 177 | 168 |
// User contains the uid and gid representing a Unix user |
| 169 |
+// TODO Windows: Factor out User |
|
| 178 | 170 |
type User struct {
|
| 179 | 171 |
UID int `json:"root_uid"` |
| 180 | 172 |
GID int `json:"root_gid"` |
| ... | ... |
@@ -18,6 +18,15 @@ import ( |
| 18 | 18 |
"github.com/opencontainers/runc/libcontainer/configs" |
| 19 | 19 |
) |
| 20 | 20 |
|
| 21 |
+// Mount contains information for a mount operation. |
|
| 22 |
+type Mount struct {
|
|
| 23 |
+ Source string `json:"source"` |
|
| 24 |
+ Destination string `json:"destination"` |
|
| 25 |
+ Writable bool `json:"writable"` |
|
| 26 |
+ Private bool `json:"private"` |
|
| 27 |
+ Slave bool `json:"slave"` |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 21 | 30 |
// Network settings of the container |
| 22 | 31 |
type Network struct {
|
| 23 | 32 |
Mtu int `json:"mtu"` |
| ... | ... |
@@ -2,6 +2,13 @@ package execdriver |
| 2 | 2 |
|
| 3 | 3 |
import "github.com/docker/docker/pkg/nat" |
| 4 | 4 |
|
| 5 |
+// Mount contains information for a mount operation. |
|
| 6 |
+type Mount struct {
|
|
| 7 |
+ Source string `json:"source"` |
|
| 8 |
+ Destination string `json:"destination"` |
|
| 9 |
+ Writable bool `json:"writable"` |
|
| 10 |
+} |
|
| 11 |
+ |
|
| 5 | 12 |
// Network settings of the container |
| 6 | 13 |
type Network struct {
|
| 7 | 14 |
Interface *NetworkInterface `json:"interface"` |
| ... | ... |
@@ -2,8 +2,6 @@ |
| 2 | 2 |
|
| 3 | 3 |
package windows |
| 4 | 4 |
|
| 5 |
-// Note this is alpha code for the bring up of containers on Windows. |
|
| 6 |
- |
|
| 7 | 5 |
import ( |
| 8 | 6 |
"encoding/json" |
| 9 | 7 |
"errors" |
| ... | ... |
@@ -60,18 +58,25 @@ type device struct {
|
| 60 | 60 |
Settings interface{}
|
| 61 | 61 |
} |
| 62 | 62 |
|
| 63 |
+type mappedDir struct {
|
|
| 64 |
+ HostPath string |
|
| 65 |
+ ContainerPath string |
|
| 66 |
+ ReadOnly bool |
|
| 67 |
+} |
|
| 68 |
+ |
|
| 63 | 69 |
type containerInit struct {
|
| 64 |
- SystemType string // HCS requires this to be hard-coded to "Container" |
|
| 65 |
- Name string // Name of the container. We use the docker ID. |
|
| 66 |
- Owner string // The management platform that created this container |
|
| 67 |
- IsDummy bool // Used for development purposes. |
|
| 68 |
- VolumePath string // Windows volume path for scratch space |
|
| 69 |
- Devices []device // Devices used by the container |
|
| 70 |
- IgnoreFlushesDuringBoot bool // Optimisation hint for container startup in Windows |
|
| 71 |
- LayerFolderPath string // Where the layer folders are located |
|
| 72 |
- Layers []layer // List of storage layers |
|
| 73 |
- ProcessorWeight int64 // CPU Shares 1..9 on Windows; or 0 is platform default. |
|
| 74 |
- HostName string // Hostname |
|
| 70 |
+ SystemType string // HCS requires this to be hard-coded to "Container" |
|
| 71 |
+ Name string // Name of the container. We use the docker ID. |
|
| 72 |
+ Owner string // The management platform that created this container |
|
| 73 |
+ IsDummy bool // Used for development purposes. |
|
| 74 |
+ VolumePath string // Windows volume path for scratch space |
|
| 75 |
+ Devices []device // Devices used by the container |
|
| 76 |
+ IgnoreFlushesDuringBoot bool // Optimisation hint for container startup in Windows |
|
| 77 |
+ LayerFolderPath string // Where the layer folders are located |
|
| 78 |
+ Layers []layer // List of storage layers |
|
| 79 |
+ ProcessorWeight int64 // CPU Shares 1..9 on Windows; or 0 is platform default. |
|
| 80 |
+ HostName string // Hostname |
|
| 81 |
+ MappedDirectories []mappedDir // List of mapped directories (volumes/mounts) |
|
| 75 | 82 |
} |
| 76 | 83 |
|
| 77 | 84 |
// defaultOwner is a tag passed to HCS to allow it to differentiate between |
| ... | ... |
@@ -105,18 +110,28 @@ func (d *Driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, hooks execd |
| 105 | 105 |
HostName: c.Hostname, |
| 106 | 106 |
} |
| 107 | 107 |
|
| 108 |
- for i := 0; i < len(c.LayerPaths); i++ {
|
|
| 109 |
- _, filename := filepath.Split(c.LayerPaths[i]) |
|
| 108 |
+ for _, layerPath := range c.LayerPaths {
|
|
| 109 |
+ _, filename := filepath.Split(layerPath) |
|
| 110 | 110 |
g, err := hcsshim.NameToGuid(filename) |
| 111 | 111 |
if err != nil {
|
| 112 | 112 |
return execdriver.ExitStatus{ExitCode: -1}, err
|
| 113 | 113 |
} |
| 114 | 114 |
cu.Layers = append(cu.Layers, layer{
|
| 115 | 115 |
ID: g.ToString(), |
| 116 |
- Path: c.LayerPaths[i], |
|
| 116 |
+ Path: layerPath, |
|
| 117 | 117 |
}) |
| 118 | 118 |
} |
| 119 | 119 |
|
| 120 |
+ // Add the mounts (volumes, bind mounts etc) to the structure |
|
| 121 |
+ mds := make([]mappedDir, len(c.Mounts)) |
|
| 122 |
+ for i, mount := range c.Mounts {
|
|
| 123 |
+ mds[i] = mappedDir{
|
|
| 124 |
+ HostPath: mount.Source, |
|
| 125 |
+ ContainerPath: mount.Destination, |
|
| 126 |
+ ReadOnly: !mount.Writable} |
|
| 127 |
+ } |
|
| 128 |
+ cu.MappedDirectories = mds |
|
| 129 |
+ |
|
| 120 | 130 |
// TODO Windows. At some point, when there is CLI on docker run to |
| 121 | 131 |
// enable the IP Address of the container to be passed into docker run, |
| 122 | 132 |
// the IP Address needs to be wired through to HCS in the JSON. It |
| ... | ... |
@@ -8,7 +8,17 @@ func setPlatformSpecificContainerFields(container *Container, contJSONBase *type |
| 8 | 8 |
} |
| 9 | 9 |
|
| 10 | 10 |
func addMountPoints(container *Container) []types.MountPoint {
|
| 11 |
- return nil |
|
| 11 |
+ mountPoints := make([]types.MountPoint, 0, len(container.MountPoints)) |
|
| 12 |
+ for _, m := range container.MountPoints {
|
|
| 13 |
+ mountPoints = append(mountPoints, types.MountPoint{
|
|
| 14 |
+ Name: m.Name, |
|
| 15 |
+ Source: m.Path(), |
|
| 16 |
+ Destination: m.Destination, |
|
| 17 |
+ Driver: m.Driver, |
|
| 18 |
+ RW: m.RW, |
|
| 19 |
+ }) |
|
| 20 |
+ } |
|
| 21 |
+ return mountPoints |
|
| 12 | 22 |
} |
| 13 | 23 |
|
| 14 | 24 |
// ContainerInspectPre120 get containers for pre 1.20 APIs. |
| ... | ... |
@@ -2,18 +2,16 @@ package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"errors" |
| 5 |
- "fmt" |
|
| 6 |
- "io/ioutil" |
|
| 7 | 5 |
"os" |
| 8 | 6 |
"path/filepath" |
| 9 | 7 |
"strings" |
| 10 | 8 |
|
| 11 |
- "github.com/Sirupsen/logrus" |
|
| 12 | 9 |
"github.com/docker/docker/api/types" |
| 10 |
+ "github.com/docker/docker/daemon/execdriver" |
|
| 13 | 11 |
derr "github.com/docker/docker/errors" |
| 14 |
- "github.com/docker/docker/pkg/chrootarchive" |
|
| 15 |
- "github.com/docker/docker/pkg/system" |
|
| 12 |
+ "github.com/docker/docker/runconfig" |
|
| 16 | 13 |
"github.com/docker/docker/volume" |
| 14 |
+ "github.com/opencontainers/runc/libcontainer/label" |
|
| 17 | 15 |
) |
| 18 | 16 |
|
| 19 | 17 |
var ( |
| ... | ... |
@@ -22,88 +20,136 @@ var ( |
| 22 | 22 |
ErrVolumeReadonly = errors.New("mounted volume is marked read-only")
|
| 23 | 23 |
) |
| 24 | 24 |
|
| 25 |
-// mountPoint is the intersection point between a volume and a container. It |
|
| 26 |
-// specifies which volume is to be used and where inside a container it should |
|
| 27 |
-// be mounted. |
|
| 28 |
-type mountPoint struct {
|
|
| 29 |
- Name string |
|
| 30 |
- Destination string |
|
| 31 |
- Driver string |
|
| 32 |
- RW bool |
|
| 33 |
- Volume volume.Volume `json:"-"` |
|
| 34 |
- Source string |
|
| 35 |
- Mode string `json:"Relabel"` // Originally field was `Relabel`" |
|
| 36 |
-} |
|
| 25 |
+type mounts []execdriver.Mount |
|
| 37 | 26 |
|
| 38 |
-// Setup sets up a mount point by either mounting the volume if it is |
|
| 39 |
-// configured, or creating the source directory if supplied. |
|
| 40 |
-func (m *mountPoint) Setup() (string, error) {
|
|
| 41 |
- if m.Volume != nil {
|
|
| 42 |
- return m.Volume.Mount() |
|
| 27 |
+// volumeToAPIType converts a volume.Volume to the type used by the remote API |
|
| 28 |
+func volumeToAPIType(v volume.Volume) *types.Volume {
|
|
| 29 |
+ return &types.Volume{
|
|
| 30 |
+ Name: v.Name(), |
|
| 31 |
+ Driver: v.DriverName(), |
|
| 32 |
+ Mountpoint: v.Path(), |
|
| 43 | 33 |
} |
| 34 |
+} |
|
| 44 | 35 |
|
| 45 |
- if len(m.Source) > 0 {
|
|
| 46 |
- if _, err := os.Stat(m.Source); err != nil {
|
|
| 47 |
- if !os.IsNotExist(err) {
|
|
| 48 |
- return "", err |
|
| 49 |
- } |
|
| 50 |
- logrus.Warnf("Auto-creating non-existant volume host path %s, this is deprecated and will be removed soon", m.Source)
|
|
| 51 |
- if err := system.MkdirAll(m.Source, 0755); err != nil {
|
|
| 52 |
- return "", err |
|
| 53 |
- } |
|
| 54 |
- } |
|
| 55 |
- return m.Source, nil |
|
| 36 |
+// createVolume creates a volume. |
|
| 37 |
+func (daemon *Daemon) createVolume(name, driverName string, opts map[string]string) (volume.Volume, error) {
|
|
| 38 |
+ v, err := daemon.volumes.Create(name, driverName, opts) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return nil, err |
|
| 56 | 41 |
} |
| 42 |
+ daemon.volumes.Increment(v) |
|
| 43 |
+ return v, nil |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+// Len returns the number of mounts. Used in sorting. |
|
| 47 |
+func (m mounts) Len() int {
|
|
| 48 |
+ return len(m) |
|
| 49 |
+} |
|
| 57 | 50 |
|
| 58 |
- return "", derr.ErrorCodeMountSetup |
|
| 51 |
+// Less returns true if the number of parts (a/b/c would be 3 parts) in the |
|
| 52 |
+// mount indexed by parameter 1 is less than that of the mount indexed by |
|
| 53 |
+// parameter 2. Used in sorting. |
|
| 54 |
+func (m mounts) Less(i, j int) bool {
|
|
| 55 |
+ return m.parts(i) < m.parts(j) |
|
| 59 | 56 |
} |
| 60 | 57 |
|
| 61 |
-// hasResource checks whether the given absolute path for a container is in |
|
| 62 |
-// this mount point. If the relative path starts with `../` then the resource |
|
| 63 |
-// is outside of this mount point, but we can't simply check for this prefix |
|
| 64 |
-// because it misses `..` which is also outside of the mount, so check both. |
|
| 65 |
-func (m *mountPoint) hasResource(absolutePath string) bool {
|
|
| 66 |
- relPath, err := filepath.Rel(m.Destination, absolutePath) |
|
| 58 |
+// Swap swaps two items in an array of mounts. Used in sorting |
|
| 59 |
+func (m mounts) Swap(i, j int) {
|
|
| 60 |
+ m[i], m[j] = m[j], m[i] |
|
| 61 |
+} |
|
| 67 | 62 |
|
| 68 |
- return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
|
|
| 63 |
+// parts returns the number of parts in the destination of a mount. Used in sorting. |
|
| 64 |
+func (m mounts) parts(i int) int {
|
|
| 65 |
+ return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator)) |
|
| 69 | 66 |
} |
| 70 | 67 |
|
| 71 |
-// Path returns the path of a volume in a mount point. |
|
| 72 |
-func (m *mountPoint) Path() string {
|
|
| 73 |
- if m.Volume != nil {
|
|
| 74 |
- return m.Volume.Path() |
|
| 68 |
+// registerMountPoints initializes the container mount points with the configured volumes and bind mounts. |
|
| 69 |
+// It follows the next sequence to decide what to mount in each final destination: |
|
| 70 |
+// |
|
| 71 |
+// 1. Select the previously configured mount points for the containers, if any. |
|
| 72 |
+// 2. Select the volumes mounted from another containers. Overrides previously configured mount point destination. |
|
| 73 |
+// 3. Select the bind mounts set by the client. Overrides previously configured mount point destinations. |
|
| 74 |
+func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runconfig.HostConfig) error {
|
|
| 75 |
+ binds := map[string]bool{}
|
|
| 76 |
+ mountPoints := map[string]*volume.MountPoint{}
|
|
| 77 |
+ |
|
| 78 |
+ // 1. Read already configured mount points. |
|
| 79 |
+ for name, point := range container.MountPoints {
|
|
| 80 |
+ mountPoints[name] = point |
|
| 75 | 81 |
} |
| 76 | 82 |
|
| 77 |
- return m.Source |
|
| 78 |
-} |
|
| 83 |
+ // 2. Read volumes from other containers. |
|
| 84 |
+ for _, v := range hostConfig.VolumesFrom {
|
|
| 85 |
+ containerID, mode, err := volume.ParseVolumesFrom(v) |
|
| 86 |
+ if err != nil {
|
|
| 87 |
+ return err |
|
| 88 |
+ } |
|
| 79 | 89 |
|
| 80 |
-// copyExistingContents copies from the source to the destination and |
|
| 81 |
-// ensures the ownership is appropriately set. |
|
| 82 |
-func copyExistingContents(source, destination string) error {
|
|
| 83 |
- volList, err := ioutil.ReadDir(source) |
|
| 84 |
- if err != nil {
|
|
| 85 |
- return err |
|
| 90 |
+ c, err := daemon.Get(containerID) |
|
| 91 |
+ if err != nil {
|
|
| 92 |
+ return err |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ for _, m := range c.MountPoints {
|
|
| 96 |
+ cp := &volume.MountPoint{
|
|
| 97 |
+ Name: m.Name, |
|
| 98 |
+ Source: m.Source, |
|
| 99 |
+ RW: m.RW && volume.ReadWrite(mode), |
|
| 100 |
+ Driver: m.Driver, |
|
| 101 |
+ Destination: m.Destination, |
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ if len(cp.Source) == 0 {
|
|
| 105 |
+ v, err := daemon.createVolume(cp.Name, cp.Driver, nil) |
|
| 106 |
+ if err != nil {
|
|
| 107 |
+ return err |
|
| 108 |
+ } |
|
| 109 |
+ cp.Volume = v |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ mountPoints[cp.Destination] = cp |
|
| 113 |
+ } |
|
| 86 | 114 |
} |
| 87 |
- if len(volList) > 0 {
|
|
| 88 |
- srcList, err := ioutil.ReadDir(destination) |
|
| 115 |
+ |
|
| 116 |
+ // 3. Read bind mounts |
|
| 117 |
+ for _, b := range hostConfig.Binds {
|
|
| 118 |
+ // #10618 |
|
| 119 |
+ bind, err := volume.ParseMountSpec(b, hostConfig.VolumeDriver) |
|
| 89 | 120 |
if err != nil {
|
| 90 | 121 |
return err |
| 91 | 122 |
} |
| 92 |
- if len(srcList) == 0 {
|
|
| 93 |
- // If the source volume is empty copy files from the root into the volume |
|
| 94 |
- if err := chrootarchive.CopyWithTar(source, destination); err != nil {
|
|
| 123 |
+ |
|
| 124 |
+ if binds[bind.Destination] {
|
|
| 125 |
+ return derr.ErrorCodeVolumeDup.WithArgs(bind.Destination) |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ if len(bind.Name) > 0 && len(bind.Driver) > 0 {
|
|
| 129 |
+ // create the volume |
|
| 130 |
+ v, err := daemon.createVolume(bind.Name, bind.Driver, nil) |
|
| 131 |
+ if err != nil {
|
|
| 95 | 132 |
return err |
| 96 | 133 |
} |
| 134 |
+ bind.Volume = v |
|
| 135 |
+ bind.Source = v.Path() |
|
| 136 |
+ // bind.Name is an already existing volume, we need to use that here |
|
| 137 |
+ bind.Driver = v.DriverName() |
|
| 138 |
+ bind = setBindModeIfNull(bind) |
|
| 139 |
+ } |
|
| 140 |
+ shared := label.IsShared(bind.Mode) |
|
| 141 |
+ if err := label.Relabel(bind.Source, container.MountLabel, shared); err != nil {
|
|
| 142 |
+ return err |
|
| 97 | 143 |
} |
| 144 |
+ binds[bind.Destination] = true |
|
| 145 |
+ mountPoints[bind.Destination] = bind |
|
| 98 | 146 |
} |
| 99 |
- return copyOwnership(source, destination) |
|
| 100 |
-} |
|
| 101 | 147 |
|
| 102 |
-// volumeToAPIType converts a volume.Volume to the type used by the remote API |
|
| 103 |
-func volumeToAPIType(v volume.Volume) *types.Volume {
|
|
| 104 |
- return &types.Volume{
|
|
| 105 |
- Name: v.Name(), |
|
| 106 |
- Driver: v.DriverName(), |
|
| 107 |
- Mountpoint: v.Path(), |
|
| 108 |
- } |
|
| 148 |
+ bcVolumes, bcVolumesRW := configureBackCompatStructures(daemon, container, mountPoints) |
|
| 149 |
+ |
|
| 150 |
+ container.Lock() |
|
| 151 |
+ container.MountPoints = mountPoints |
|
| 152 |
+ setBackCompatStructures(container, bcVolumes, bcVolumesRW) |
|
| 153 |
+ |
|
| 154 |
+ container.Unlock() |
|
| 155 |
+ |
|
| 156 |
+ return nil |
|
| 109 | 157 |
} |
| 110 | 158 |
deleted file mode 100644 |
| ... | ... |
@@ -1,58 +0,0 @@ |
| 1 |
-// +build experimental |
|
| 2 |
- |
|
| 3 |
-package daemon |
|
| 4 |
- |
|
| 5 |
-import "testing" |
|
| 6 |
- |
|
| 7 |
-func TestParseBindMount(t *testing.T) {
|
|
| 8 |
- cases := []struct {
|
|
| 9 |
- bind string |
|
| 10 |
- driver string |
|
| 11 |
- expDest string |
|
| 12 |
- expSource string |
|
| 13 |
- expName string |
|
| 14 |
- expDriver string |
|
| 15 |
- expRW bool |
|
| 16 |
- fail bool |
|
| 17 |
- }{
|
|
| 18 |
- {"/tmp:/tmp", "", "/tmp", "/tmp", "", "", true, false},
|
|
| 19 |
- {"/tmp:/tmp:ro", "", "/tmp", "/tmp", "", "", false, false},
|
|
| 20 |
- {"/tmp:/tmp:rw", "", "/tmp", "/tmp", "", "", true, false},
|
|
| 21 |
- {"/tmp:/tmp:foo", "", "/tmp", "/tmp", "", "", false, true},
|
|
| 22 |
- {"name:/tmp", "", "/tmp", "", "name", "local", true, false},
|
|
| 23 |
- {"name:/tmp", "external", "/tmp", "", "name", "external", true, false},
|
|
| 24 |
- {"name:/tmp:ro", "local", "/tmp", "", "name", "local", false, false},
|
|
| 25 |
- {"local/name:/tmp:rw", "", "/tmp", "", "local/name", "local", true, false},
|
|
| 26 |
- {"/tmp:tmp", "", "", "", "", "", true, true},
|
|
| 27 |
- } |
|
| 28 |
- |
|
| 29 |
- for _, c := range cases {
|
|
| 30 |
- m, err := parseBindMount(c.bind, c.driver) |
|
| 31 |
- if c.fail {
|
|
| 32 |
- if err == nil {
|
|
| 33 |
- t.Fatalf("Expected error, was nil, for spec %s\n", c.bind)
|
|
| 34 |
- } |
|
| 35 |
- continue |
|
| 36 |
- } |
|
| 37 |
- |
|
| 38 |
- if m.Destination != c.expDest {
|
|
| 39 |
- t.Fatalf("Expected destination %s, was %s, for spec %s\n", c.expDest, m.Destination, c.bind)
|
|
| 40 |
- } |
|
| 41 |
- |
|
| 42 |
- if m.Source != c.expSource {
|
|
| 43 |
- t.Fatalf("Expected source %s, was %s, for spec %s\n", c.expSource, m.Source, c.bind)
|
|
| 44 |
- } |
|
| 45 |
- |
|
| 46 |
- if m.Name != c.expName {
|
|
| 47 |
- t.Fatalf("Expected name %s, was %s for spec %s\n", c.expName, m.Name, c.bind)
|
|
| 48 |
- } |
|
| 49 |
- |
|
| 50 |
- if m.Driver != c.expDriver {
|
|
| 51 |
- t.Fatalf("Expected driver %s, was %s, for spec %s\n", c.expDriver, m.Driver, c.bind)
|
|
| 52 |
- } |
|
| 53 |
- |
|
| 54 |
- if m.RW != c.expRW {
|
|
| 55 |
- t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind)
|
|
| 56 |
- } |
|
| 57 |
- } |
|
| 58 |
-} |
| ... | ... |
@@ -1,6 +1,9 @@ |
| 1 | 1 |
package daemon |
| 2 | 2 |
|
| 3 |
-import "testing" |
|
| 3 |
+import ( |
|
| 4 |
+ "github.com/docker/docker/volume" |
|
| 5 |
+ "testing" |
|
| 6 |
+) |
|
| 4 | 7 |
|
| 5 | 8 |
func TestParseVolumesFrom(t *testing.T) {
|
| 6 | 9 |
cases := []struct {
|
| ... | ... |
@@ -17,7 +20,7 @@ func TestParseVolumesFrom(t *testing.T) {
|
| 17 | 17 |
} |
| 18 | 18 |
|
| 19 | 19 |
for _, c := range cases {
|
| 20 |
- id, mode, err := parseVolumesFrom(c.spec) |
|
| 20 |
+ id, mode, err := volume.ParseVolumesFrom(c.spec) |
|
| 21 | 21 |
if c.fail {
|
| 22 | 22 |
if err == nil {
|
| 23 | 23 |
t.Fatalf("Expected error, was nil, for spec %s\n", c.spec)
|
| ... | ... |
@@ -11,15 +11,35 @@ import ( |
| 11 | 11 |
|
| 12 | 12 |
"github.com/Sirupsen/logrus" |
| 13 | 13 |
"github.com/docker/docker/daemon/execdriver" |
| 14 |
- derr "github.com/docker/docker/errors" |
|
| 14 |
+ "github.com/docker/docker/pkg/chrootarchive" |
|
| 15 | 15 |
"github.com/docker/docker/pkg/system" |
| 16 |
- "github.com/docker/docker/runconfig" |
|
| 17 | 16 |
"github.com/docker/docker/volume" |
| 18 | 17 |
volumedrivers "github.com/docker/docker/volume/drivers" |
| 19 | 18 |
"github.com/docker/docker/volume/local" |
| 20 |
- "github.com/opencontainers/runc/libcontainer/label" |
|
| 21 | 19 |
) |
| 22 | 20 |
|
| 21 |
+// copyExistingContents copies from the source to the destination and |
|
| 22 |
+// ensures the ownership is appropriately set. |
|
| 23 |
+func copyExistingContents(source, destination string) error {
|
|
| 24 |
+ volList, err := ioutil.ReadDir(source) |
|
| 25 |
+ if err != nil {
|
|
| 26 |
+ return err |
|
| 27 |
+ } |
|
| 28 |
+ if len(volList) > 0 {
|
|
| 29 |
+ srcList, err := ioutil.ReadDir(destination) |
|
| 30 |
+ if err != nil {
|
|
| 31 |
+ return err |
|
| 32 |
+ } |
|
| 33 |
+ if len(srcList) == 0 {
|
|
| 34 |
+ // If the source volume is empty copy files from the root into the volume |
|
| 35 |
+ if err := chrootarchive.CopyWithTar(source, destination); err != nil {
|
|
| 36 |
+ return err |
|
| 37 |
+ } |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+ return copyOwnership(source, destination) |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 23 | 43 |
// copyOwnership copies the permissions and uid:gid of the source file |
| 24 | 44 |
// to the destination file |
| 25 | 45 |
func copyOwnership(source, destination string) error {
|
| ... | ... |
@@ -68,53 +88,6 @@ func (container *Container) setupMounts() ([]execdriver.Mount, error) {
|
| 68 | 68 |
return append(mounts, netMounts...), nil |
| 69 | 69 |
} |
| 70 | 70 |
|
| 71 |
-// parseBindMount validates the configuration of mount information in runconfig is valid. |
|
| 72 |
-func parseBindMount(spec, volumeDriver string) (*mountPoint, error) {
|
|
| 73 |
- bind := &mountPoint{
|
|
| 74 |
- RW: true, |
|
| 75 |
- } |
|
| 76 |
- arr := strings.Split(spec, ":") |
|
| 77 |
- |
|
| 78 |
- switch len(arr) {
|
|
| 79 |
- case 2: |
|
| 80 |
- bind.Destination = arr[1] |
|
| 81 |
- case 3: |
|
| 82 |
- bind.Destination = arr[1] |
|
| 83 |
- mode := arr[2] |
|
| 84 |
- if !volume.ValidMountMode(mode) {
|
|
| 85 |
- return nil, derr.ErrorCodeVolumeInvalidMode.WithArgs(mode) |
|
| 86 |
- } |
|
| 87 |
- bind.RW = volume.ReadWrite(mode) |
|
| 88 |
- // Mode field is used by SELinux to decide whether to apply label |
|
| 89 |
- bind.Mode = mode |
|
| 90 |
- default: |
|
| 91 |
- return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) |
|
| 92 |
- } |
|
| 93 |
- |
|
| 94 |
- //validate the volumes destination path |
|
| 95 |
- if !filepath.IsAbs(bind.Destination) {
|
|
| 96 |
- return nil, derr.ErrorCodeVolumeAbs.WithArgs(bind.Destination) |
|
| 97 |
- } |
|
| 98 |
- |
|
| 99 |
- name, source, err := parseVolumeSource(arr[0]) |
|
| 100 |
- if err != nil {
|
|
| 101 |
- return nil, err |
|
| 102 |
- } |
|
| 103 |
- |
|
| 104 |
- if len(source) == 0 {
|
|
| 105 |
- bind.Driver = volumeDriver |
|
| 106 |
- if len(bind.Driver) == 0 {
|
|
| 107 |
- bind.Driver = volume.DefaultDriverName |
|
| 108 |
- } |
|
| 109 |
- } else {
|
|
| 110 |
- bind.Source = filepath.Clean(source) |
|
| 111 |
- } |
|
| 112 |
- |
|
| 113 |
- bind.Name = name |
|
| 114 |
- bind.Destination = filepath.Clean(bind.Destination) |
|
| 115 |
- return bind, nil |
|
| 116 |
-} |
|
| 117 |
- |
|
| 118 | 71 |
// sortMounts sorts an array of mounts in lexicographic order. This ensure that |
| 119 | 72 |
// when mounting, the mounts don't shadow other mounts. For example, if mounting |
| 120 | 73 |
// /etc and /etc/resolv.conf, /etc/resolv.conf must not be mounted first. |
| ... | ... |
@@ -123,30 +96,6 @@ func sortMounts(m []execdriver.Mount) []execdriver.Mount {
|
| 123 | 123 |
return m |
| 124 | 124 |
} |
| 125 | 125 |
|
| 126 |
-type mounts []execdriver.Mount |
|
| 127 |
- |
|
| 128 |
-// Len returns the number of mounts |
|
| 129 |
-func (m mounts) Len() int {
|
|
| 130 |
- return len(m) |
|
| 131 |
-} |
|
| 132 |
- |
|
| 133 |
-// Less returns true if the number of parts (a/b/c would be 3 parts) in the |
|
| 134 |
-// mount indexed by parameter 1 is less than that of the mount indexed by |
|
| 135 |
-// parameter 2. |
|
| 136 |
-func (m mounts) Less(i, j int) bool {
|
|
| 137 |
- return m.parts(i) < m.parts(j) |
|
| 138 |
-} |
|
| 139 |
- |
|
| 140 |
-// Swap swaps two items in an array of mounts. |
|
| 141 |
-func (m mounts) Swap(i, j int) {
|
|
| 142 |
- m[i], m[j] = m[j], m[i] |
|
| 143 |
-} |
|
| 144 |
- |
|
| 145 |
-// parts returns the number of parts in the destination of a mount. |
|
| 146 |
-func (m mounts) parts(i int) int {
|
|
| 147 |
- return len(strings.Split(filepath.Clean(m[i].Destination), string(os.PathSeparator))) |
|
| 148 |
-} |
|
| 149 |
- |
|
| 150 | 126 |
// migrateVolume links the contents of a volume created pre Docker 1.7 |
| 151 | 127 |
// into the location expected by the local driver. |
| 152 | 128 |
// It creates a symlink from DOCKER_ROOT/vfs/dir/VOLUME_ID to DOCKER_ROOT/volumes/VOLUME_ID/_container_data. |
| ... | ... |
@@ -211,12 +160,7 @@ func (daemon *Daemon) verifyVolumesInfo(container *Container) error {
|
| 211 | 211 |
} |
| 212 | 212 |
container.addLocalMountPoint(id, destination, rw) |
| 213 | 213 |
} else { // Bind mount
|
| 214 |
- id, source, err := parseVolumeSource(hostPath) |
|
| 215 |
- // We should not find an error here coming |
|
| 216 |
- // from the old configuration, but who knows. |
|
| 217 |
- if err != nil {
|
|
| 218 |
- return err |
|
| 219 |
- } |
|
| 214 |
+ id, source := volume.ParseVolumeSource(hostPath) |
|
| 220 | 215 |
container.addBindMountPoint(id, source, destination, rw) |
| 221 | 216 |
} |
| 222 | 217 |
} |
| ... | ... |
@@ -270,109 +214,19 @@ func (daemon *Daemon) verifyVolumesInfo(container *Container) error {
|
| 270 | 270 |
return nil |
| 271 | 271 |
} |
| 272 | 272 |
|
| 273 |
-// parseVolumesFrom ensure that the supplied volumes-from is valid. |
|
| 274 |
-func parseVolumesFrom(spec string) (string, string, error) {
|
|
| 275 |
- if len(spec) == 0 {
|
|
| 276 |
- return "", "", derr.ErrorCodeVolumeFromBlank.WithArgs(spec) |
|
| 273 |
+// setBindModeIfNull is platform specific processing to ensure the |
|
| 274 |
+// shared mode is set to 'z' if it is null. This is called in the case |
|
| 275 |
+// of processing a named volume and not a typical bind. |
|
| 276 |
+func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint {
|
|
| 277 |
+ if bind.Mode == "" {
|
|
| 278 |
+ bind.Mode = "z" |
|
| 277 | 279 |
} |
| 278 |
- |
|
| 279 |
- specParts := strings.SplitN(spec, ":", 2) |
|
| 280 |
- id := specParts[0] |
|
| 281 |
- mode := "rw" |
|
| 282 |
- |
|
| 283 |
- if len(specParts) == 2 {
|
|
| 284 |
- mode = specParts[1] |
|
| 285 |
- if !volume.ValidMountMode(mode) {
|
|
| 286 |
- return "", "", derr.ErrorCodeVolumeMode.WithArgs(mode) |
|
| 287 |
- } |
|
| 288 |
- } |
|
| 289 |
- return id, mode, nil |
|
| 280 |
+ return bind |
|
| 290 | 281 |
} |
| 291 | 282 |
|
| 292 |
-// registerMountPoints initializes the container mount points with the configured volumes and bind mounts. |
|
| 293 |
-// It follows the next sequence to decide what to mount in each final destination: |
|
| 294 |
-// |
|
| 295 |
-// 1. Select the previously configured mount points for the containers, if any. |
|
| 296 |
-// 2. Select the volumes mounted from another containers. Overrides previously configured mount point destination. |
|
| 297 |
-// 3. Select the bind mounts set by the client. Overrides previously configured mount point destinations. |
|
| 298 |
-func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runconfig.HostConfig) error {
|
|
| 299 |
- binds := map[string]bool{}
|
|
| 300 |
- mountPoints := map[string]*mountPoint{}
|
|
| 301 |
- |
|
| 302 |
- // 1. Read already configured mount points. |
|
| 303 |
- for name, point := range container.MountPoints {
|
|
| 304 |
- mountPoints[name] = point |
|
| 305 |
- } |
|
| 306 |
- |
|
| 307 |
- // 2. Read volumes from other containers. |
|
| 308 |
- for _, v := range hostConfig.VolumesFrom {
|
|
| 309 |
- containerID, mode, err := parseVolumesFrom(v) |
|
| 310 |
- if err != nil {
|
|
| 311 |
- return err |
|
| 312 |
- } |
|
| 313 |
- |
|
| 314 |
- c, err := daemon.Get(containerID) |
|
| 315 |
- if err != nil {
|
|
| 316 |
- return err |
|
| 317 |
- } |
|
| 318 |
- |
|
| 319 |
- for _, m := range c.MountPoints {
|
|
| 320 |
- cp := &mountPoint{
|
|
| 321 |
- Name: m.Name, |
|
| 322 |
- Source: m.Source, |
|
| 323 |
- RW: m.RW && volume.ReadWrite(mode), |
|
| 324 |
- Driver: m.Driver, |
|
| 325 |
- Destination: m.Destination, |
|
| 326 |
- } |
|
| 327 |
- |
|
| 328 |
- if len(cp.Source) == 0 {
|
|
| 329 |
- v, err := daemon.createVolume(cp.Name, cp.Driver, nil) |
|
| 330 |
- if err != nil {
|
|
| 331 |
- return err |
|
| 332 |
- } |
|
| 333 |
- cp.Volume = v |
|
| 334 |
- } |
|
| 335 |
- |
|
| 336 |
- mountPoints[cp.Destination] = cp |
|
| 337 |
- } |
|
| 338 |
- } |
|
| 339 |
- |
|
| 340 |
- // 3. Read bind mounts |
|
| 341 |
- for _, b := range hostConfig.Binds {
|
|
| 342 |
- // #10618 |
|
| 343 |
- bind, err := parseBindMount(b, hostConfig.VolumeDriver) |
|
| 344 |
- if err != nil {
|
|
| 345 |
- return err |
|
| 346 |
- } |
|
| 347 |
- |
|
| 348 |
- if binds[bind.Destination] {
|
|
| 349 |
- return derr.ErrorCodeVolumeDup.WithArgs(bind.Destination) |
|
| 350 |
- } |
|
| 351 |
- |
|
| 352 |
- if len(bind.Name) > 0 && len(bind.Driver) > 0 {
|
|
| 353 |
- // create the volume |
|
| 354 |
- v, err := daemon.createVolume(bind.Name, bind.Driver, nil) |
|
| 355 |
- if err != nil {
|
|
| 356 |
- return err |
|
| 357 |
- } |
|
| 358 |
- bind.Volume = v |
|
| 359 |
- bind.Source = v.Path() |
|
| 360 |
- // bind.Name is an already existing volume, we need to use that here |
|
| 361 |
- bind.Driver = v.DriverName() |
|
| 362 |
- // Since this is just a named volume and not a typical bind, set to shared mode `z` |
|
| 363 |
- if bind.Mode == "" {
|
|
| 364 |
- bind.Mode = "z" |
|
| 365 |
- } |
|
| 366 |
- } |
|
| 367 |
- |
|
| 368 |
- shared := label.IsShared(bind.Mode) |
|
| 369 |
- if err := label.Relabel(bind.Source, container.MountLabel, shared); err != nil {
|
|
| 370 |
- return err |
|
| 371 |
- } |
|
| 372 |
- binds[bind.Destination] = true |
|
| 373 |
- mountPoints[bind.Destination] = bind |
|
| 374 |
- } |
|
| 375 |
- |
|
| 283 |
+// configureBackCompatStructures is platform specific processing for |
|
| 284 |
+// registering mount points to populate old structures. |
|
| 285 |
+func configureBackCompatStructures(daemon *Daemon, container *Container, mountPoints map[string]*volume.MountPoint) (map[string]string, map[string]bool) {
|
|
| 376 | 286 |
// Keep backwards compatible structures |
| 377 | 287 |
bcVolumes := map[string]string{}
|
| 378 | 288 |
bcVolumesRW := map[string]bool{}
|
| ... | ... |
@@ -387,38 +241,12 @@ func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runc |
| 387 | 387 |
} |
| 388 | 388 |
} |
| 389 | 389 |
} |
| 390 |
+ return bcVolumes, bcVolumesRW |
|
| 391 |
+} |
|
| 390 | 392 |
|
| 391 |
- container.Lock() |
|
| 392 |
- container.MountPoints = mountPoints |
|
| 393 |
+// setBackCompatStructures is a platform specific helper function to set |
|
| 394 |
+// backwards compatible structures in the container when registering volumes. |
|
| 395 |
+func setBackCompatStructures(container *Container, bcVolumes map[string]string, bcVolumesRW map[string]bool) {
|
|
| 393 | 396 |
container.Volumes = bcVolumes |
| 394 | 397 |
container.VolumesRW = bcVolumesRW |
| 395 |
- container.Unlock() |
|
| 396 |
- |
|
| 397 |
- return nil |
|
| 398 |
-} |
|
| 399 |
- |
|
| 400 |
-// createVolume creates a volume. |
|
| 401 |
-func (daemon *Daemon) createVolume(name, driverName string, opts map[string]string) (volume.Volume, error) {
|
|
| 402 |
- v, err := daemon.volumes.Create(name, driverName, opts) |
|
| 403 |
- if err != nil {
|
|
| 404 |
- return nil, err |
|
| 405 |
- } |
|
| 406 |
- daemon.volumes.Increment(v) |
|
| 407 |
- return v, nil |
|
| 408 |
-} |
|
| 409 |
- |
|
| 410 |
-// parseVolumeSource parses the origin sources that's mounted into the container. |
|
| 411 |
-func parseVolumeSource(spec string) (string, string, error) {
|
|
| 412 |
- if !filepath.IsAbs(spec) {
|
|
| 413 |
- return spec, "", nil |
|
| 414 |
- } |
|
| 415 |
- |
|
| 416 |
- return "", spec, nil |
|
| 417 |
-} |
|
| 418 |
- |
|
| 419 |
-// BackwardsCompatible decides whether this mount point can be |
|
| 420 |
-// used in old versions of Docker or not. |
|
| 421 |
-// Only bind mounts and local volumes can be used in old versions of Docker. |
|
| 422 |
-func (m *mountPoint) BackwardsCompatible() bool {
|
|
| 423 |
- return len(m.Source) > 0 || m.Driver == volume.DefaultDriverName |
|
| 424 | 398 |
} |
| ... | ... |
@@ -4,22 +4,35 @@ package daemon |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 | 6 |
"github.com/docker/docker/daemon/execdriver" |
| 7 |
- "github.com/docker/docker/runconfig" |
|
| 7 |
+ derr "github.com/docker/docker/errors" |
|
| 8 |
+ "github.com/docker/docker/volume" |
|
| 9 |
+ "sort" |
|
| 8 | 10 |
) |
| 9 | 11 |
|
| 10 |
-// copyOwnership copies the permissions and group of a source file to the |
|
| 11 |
-// destination file. This is a no-op on Windows. |
|
| 12 |
-func copyOwnership(source, destination string) error {
|
|
| 13 |
- return nil |
|
| 14 |
-} |
|
| 15 |
- |
|
| 16 |
-// setupMounts configures the mount points for a container. |
|
| 17 |
-// setupMounts on Linux iterates through each of the mount points for a |
|
| 18 |
-// container and calls Setup() on each. It also looks to see if is a network |
|
| 19 |
-// mount such as /etc/resolv.conf, and if it is not, appends it to the array |
|
| 20 |
-// of mounts. As Windows does not support mount points, this is a no-op. |
|
| 12 |
+// setupMounts configures the mount points for a container by appending each |
|
| 13 |
+// of the configured mounts on the container to the execdriver mount structure |
|
| 14 |
+// which will ultimately be passed into the exec driver during container creation. |
|
| 15 |
+// It also ensures each of the mounts are lexographically sorted. |
|
| 21 | 16 |
func (container *Container) setupMounts() ([]execdriver.Mount, error) {
|
| 22 |
- return nil, nil |
|
| 17 |
+ var mnts []execdriver.Mount |
|
| 18 |
+ for _, mount := range container.MountPoints { // type is volume.MountPoint
|
|
| 19 |
+ // If there is no source, take it from the volume path |
|
| 20 |
+ s := mount.Source |
|
| 21 |
+ if s == "" && mount.Volume != nil {
|
|
| 22 |
+ s = mount.Volume.Path() |
|
| 23 |
+ } |
|
| 24 |
+ if s == "" {
|
|
| 25 |
+ return nil, derr.ErrorCodeVolumeNoSourceForMount.WithArgs(mount.Name, mount.Driver, mount.Destination) |
|
| 26 |
+ } |
|
| 27 |
+ mnts = append(mnts, execdriver.Mount{
|
|
| 28 |
+ Source: s, |
|
| 29 |
+ Destination: mount.Destination, |
|
| 30 |
+ Writable: mount.RW, |
|
| 31 |
+ }) |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ sort.Sort(mounts(mnts)) |
|
| 35 |
+ return mnts, nil |
|
| 23 | 36 |
} |
| 24 | 37 |
|
| 25 | 38 |
// verifyVolumesInfo ports volumes configured for the containers pre docker 1.7. |
| ... | ... |
@@ -28,9 +41,20 @@ func (daemon *Daemon) verifyVolumesInfo(container *Container) error {
|
| 28 | 28 |
return nil |
| 29 | 29 |
} |
| 30 | 30 |
|
| 31 |
-// registerMountPoints initializes the container mount points with the |
|
| 32 |
-// configured volumes and bind mounts. Windows does not support volumes or |
|
| 33 |
-// mount points. |
|
| 34 |
-func (daemon *Daemon) registerMountPoints(container *Container, hostConfig *runconfig.HostConfig) error {
|
|
| 35 |
- return nil |
|
| 31 |
+// setBindModeIfNull is platform specific processing which is a no-op on |
|
| 32 |
+// Windows. |
|
| 33 |
+func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint {
|
|
| 34 |
+ return bind |
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+// configureBackCompatStructures is platform specific processing for |
|
| 38 |
+// registering mount points to populate old structures. This is a no-op on Windows. |
|
| 39 |
+func configureBackCompatStructures(*Daemon, *Container, map[string]*volume.MountPoint) (map[string]string, map[string]bool) {
|
|
| 40 |
+ return nil, nil |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+// setBackCompatStructures is a platform specific helper function to set |
|
| 44 |
+// backwards compatible structures in the container when registering volumes. |
|
| 45 |
+// This is a no-op on Windows. |
|
| 46 |
+func setBackCompatStructures(*Container, map[string]string, map[string]bool) {
|
|
| 36 | 47 |
} |
| ... | ... |
@@ -359,12 +359,12 @@ var ( |
| 359 | 359 |
HTTPStatusCode: http.StatusInternalServerError, |
| 360 | 360 |
}) |
| 361 | 361 |
|
| 362 |
- // ErrorCodeVolumeInvalidMode is generated when we the mode of a volume |
|
| 362 |
+ // ErrorCodeVolumeInvalidMode is generated when we the mode of a volume/bind |
|
| 363 | 363 |
// mount is invalid. |
| 364 | 364 |
ErrorCodeVolumeInvalidMode = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
| 365 | 365 |
Value: "VOLUMEINVALIDMODE", |
| 366 |
- Message: "invalid mode for volumes-from: %s", |
|
| 367 |
- Description: "An invalid 'mode' was specified in the mount request", |
|
| 366 |
+ Message: "invalid mode: %s", |
|
| 367 |
+ Description: "An invalid 'mode' was specified", |
|
| 368 | 368 |
HTTPStatusCode: http.StatusInternalServerError, |
| 369 | 369 |
}) |
| 370 | 370 |
|
| ... | ... |
@@ -393,6 +393,41 @@ var ( |
| 393 | 393 |
HTTPStatusCode: http.StatusBadRequest, |
| 394 | 394 |
}) |
| 395 | 395 |
|
| 396 |
+ // ErrorCodeVolumeSlash is generated when destination path to a volume is / |
|
| 397 |
+ ErrorCodeVolumeSlash = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
|
| 398 |
+ Value: "VOLUMESLASH", |
|
| 399 |
+ Message: "Invalid specification: destination can't be '/' in '%s'", |
|
| 400 |
+ HTTPStatusCode: http.StatusInternalServerError, |
|
| 401 |
+ }) |
|
| 402 |
+ |
|
| 403 |
+ // ErrorCodeVolumeDestIsC is generated the destination is c: (Windows specific) |
|
| 404 |
+ ErrorCodeVolumeDestIsC = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
|
| 405 |
+ Value: "VOLUMEDESTISC", |
|
| 406 |
+ Message: "Destination drive letter in '%s' cannot be c:", |
|
| 407 |
+ HTTPStatusCode: http.StatusInternalServerError, |
|
| 408 |
+ }) |
|
| 409 |
+ |
|
| 410 |
+ // ErrorCodeVolumeDestIsCRoot is generated the destination path is c:\ (Windows specific) |
|
| 411 |
+ ErrorCodeVolumeDestIsCRoot = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
|
| 412 |
+ Value: "VOLUMEDESTISCROOT", |
|
| 413 |
+ Message: `Destination path in '%s' cannot be c:\`, |
|
| 414 |
+ HTTPStatusCode: http.StatusInternalServerError, |
|
| 415 |
+ }) |
|
| 416 |
+ |
|
| 417 |
+ // ErrorCodeVolumeSourceNotFound is generated the source directory could not be found (Windows specific) |
|
| 418 |
+ ErrorCodeVolumeSourceNotFound = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
|
| 419 |
+ Value: "VOLUMESOURCENOTFOUND", |
|
| 420 |
+ Message: "Source directory '%s' could not be found: %v", |
|
| 421 |
+ HTTPStatusCode: http.StatusInternalServerError, |
|
| 422 |
+ }) |
|
| 423 |
+ |
|
| 424 |
+ // ErrorCodeVolumeSourceNotDirectory is generated the source is not a directory (Windows specific) |
|
| 425 |
+ ErrorCodeVolumeSourceNotDirectory = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
|
| 426 |
+ Value: "VOLUMESOURCENOTDIRECTORY", |
|
| 427 |
+ Message: "Source '%s' is not a directory", |
|
| 428 |
+ HTTPStatusCode: http.StatusInternalServerError, |
|
| 429 |
+ }) |
|
| 430 |
+ |
|
| 396 | 431 |
// ErrorCodeVolumeFromBlank is generated when path to a volume is blank. |
| 397 | 432 |
ErrorCodeVolumeFromBlank = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
| 398 | 433 |
Value: "VOLUMEFROMBLANK", |
| ... | ... |
@@ -401,15 +436,6 @@ var ( |
| 401 | 401 |
HTTPStatusCode: http.StatusInternalServerError, |
| 402 | 402 |
}) |
| 403 | 403 |
|
| 404 |
- // ErrorCodeVolumeMode is generated when 'mode' for a volume |
|
| 405 |
- // isn't a valid. |
|
| 406 |
- ErrorCodeVolumeMode = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
|
| 407 |
- Value: "VOLUMEMODE", |
|
| 408 |
- Message: "invalid mode for volumes-from: %s", |
|
| 409 |
- Description: "An invalid 'mode' path was specified in the mount request", |
|
| 410 |
- HTTPStatusCode: http.StatusInternalServerError, |
|
| 411 |
- }) |
|
| 412 |
- |
|
| 413 | 404 |
// ErrorCodeVolumeDup is generated when we try to mount two volumes |
| 414 | 405 |
// to the same path. |
| 415 | 406 |
ErrorCodeVolumeDup = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
| ... | ... |
@@ -419,6 +445,22 @@ var ( |
| 419 | 419 |
HTTPStatusCode: http.StatusInternalServerError, |
| 420 | 420 |
}) |
| 421 | 421 |
|
| 422 |
+ // ErrorCodeVolumeNoSourceForMount is generated when no source directory |
|
| 423 |
+ // for a volume mount was found. (Windows specific) |
|
| 424 |
+ ErrorCodeVolumeNoSourceForMount = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
|
| 425 |
+ Value: "VOLUMENOSOURCEFORMOUNT", |
|
| 426 |
+ Message: "No source for mount name %q driver %q destination %s", |
|
| 427 |
+ HTTPStatusCode: http.StatusInternalServerError, |
|
| 428 |
+ }) |
|
| 429 |
+ |
|
| 430 |
+ // ErrorCodeVolumeNameReservedWord is generated when the name in a volume |
|
| 431 |
+ // uses a reserved word for filenames. (Windows specific) |
|
| 432 |
+ ErrorCodeVolumeNameReservedWord = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
|
| 433 |
+ Value: "VOLUMENAMERESERVEDWORD", |
|
| 434 |
+ Message: "Volume name %q cannot be a reserved word for Windows filenames", |
|
| 435 |
+ HTTPStatusCode: http.StatusInternalServerError, |
|
| 436 |
+ }) |
|
| 437 |
+ |
|
| 422 | 438 |
// ErrorCodeCantUnpause is generated when there's an error while trying |
| 423 | 439 |
// to unpause a container. |
| 424 | 440 |
ErrorCodeCantUnpause = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
| ... | ... |
@@ -1 +1,2 @@ |
| 1 | 1 |
{"architecture":"amd64","config":{"Hostname":"03797203757d","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":null,"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"OnBuild":[],"Labels":{}},"container":"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253","container_config":{"Hostname":"03797203757d","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":["/bin/sh","-c","#(nop) ENTRYPOINT [\"/go/bin/dnsdock\"]"],"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"OnBuild":[],"Labels":{}},"created":"2015-08-19T16:49:11.368300679Z","docker_version":"1.6.2","layer_id":"sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a","os":"linux","parent_id":"sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02"}
|
| 2 |
+ |
| ... | ... |
@@ -293,8 +293,8 @@ func (s *DockerSuite) TestRunVolumesFromInReadWriteMode(c *check.C) {
|
| 293 | 293 |
dockerCmd(c, "run", "--name", "parent", "-v", "/test", "busybox", "true") |
| 294 | 294 |
dockerCmd(c, "run", "--volumes-from", "parent:rw", "busybox", "touch", "/test/file") |
| 295 | 295 |
|
| 296 |
- if out, _, err := dockerCmdWithError("run", "--volumes-from", "parent:bar", "busybox", "touch", "/test/file"); err == nil || !strings.Contains(out, "invalid mode for volumes-from: bar") {
|
|
| 297 |
- c.Fatalf("running --volumes-from foo:bar should have failed with invalid mount mode: %q", out)
|
|
| 296 |
+ if out, _, err := dockerCmdWithError("run", "--volumes-from", "parent:bar", "busybox", "touch", "/test/file"); err == nil || !strings.Contains(out, "invalid mode: bar") {
|
|
| 297 |
+ c.Fatalf("running --volumes-from foo:bar should have failed with invalid mode: %q", out)
|
|
| 298 | 298 |
} |
| 299 | 299 |
|
| 300 | 300 |
dockerCmd(c, "run", "--volumes-from", "parent", "busybox", "touch", "/test/file") |
| ... | ... |
@@ -9,7 +9,6 @@ import ( |
| 9 | 9 |
"strings" |
| 10 | 10 |
|
| 11 | 11 |
"github.com/docker/docker/pkg/parsers" |
| 12 |
- "github.com/docker/docker/volume" |
|
| 13 | 12 |
) |
| 14 | 13 |
|
| 15 | 14 |
var ( |
| ... | ... |
@@ -214,14 +213,6 @@ func ValidateDevice(val string) (string, error) {
|
| 214 | 214 |
return validatePath(val, ValidDeviceMode) |
| 215 | 215 |
} |
| 216 | 216 |
|
| 217 |
-// ValidatePath validates a path for volumes |
|
| 218 |
-// It will make sure 'val' is in the form: |
|
| 219 |
-// [host-dir:]container-path[:rw|ro] |
|
| 220 |
-// It also validates the mount mode. |
|
| 221 |
-func ValidatePath(val string) (string, error) {
|
|
| 222 |
- return validatePath(val, volume.ValidMountMode) |
|
| 223 |
-} |
|
| 224 |
- |
|
| 225 | 217 |
func validatePath(val string, validator func(string) bool) (string, error) {
|
| 226 | 218 |
var containerPath string |
| 227 | 219 |
var mode string |
| ... | ... |
@@ -274,58 +274,6 @@ func TestValidateLink(t *testing.T) {
|
| 274 | 274 |
} |
| 275 | 275 |
} |
| 276 | 276 |
|
| 277 |
-func TestValidatePath(t *testing.T) {
|
|
| 278 |
- valid := []string{
|
|
| 279 |
- "/home", |
|
| 280 |
- "/home:/home", |
|
| 281 |
- "/home:/something/else", |
|
| 282 |
- "/with space", |
|
| 283 |
- "/home:/with space", |
|
| 284 |
- "relative:/absolute-path", |
|
| 285 |
- "hostPath:/containerPath:ro", |
|
| 286 |
- "/hostPath:/containerPath:rw", |
|
| 287 |
- "/rw:/ro", |
|
| 288 |
- "/path:rw", |
|
| 289 |
- "/path:ro", |
|
| 290 |
- "/rw:rw", |
|
| 291 |
- } |
|
| 292 |
- invalid := map[string]string{
|
|
| 293 |
- "": "bad format for path: ", |
|
| 294 |
- "./": "./ is not an absolute path", |
|
| 295 |
- "../": "../ is not an absolute path", |
|
| 296 |
- "/:../": "../ is not an absolute path", |
|
| 297 |
- "/:path": "path is not an absolute path", |
|
| 298 |
- ":": "bad format for path: :", |
|
| 299 |
- "/tmp:": " is not an absolute path", |
|
| 300 |
- ":test": "bad format for path: :test", |
|
| 301 |
- ":/test": "bad format for path: :/test", |
|
| 302 |
- "tmp:": " is not an absolute path", |
|
| 303 |
- ":test:": "bad format for path: :test:", |
|
| 304 |
- "::": "bad format for path: ::", |
|
| 305 |
- ":::": "bad format for path: :::", |
|
| 306 |
- "/tmp:::": "bad format for path: /tmp:::", |
|
| 307 |
- ":/tmp::": "bad format for path: :/tmp::", |
|
| 308 |
- "path:ro": "path is not an absolute path", |
|
| 309 |
- "/path:/path:sw": "bad mode specified: sw", |
|
| 310 |
- "/path:/path:rwz": "bad mode specified: rwz", |
|
| 311 |
- } |
|
| 312 |
- |
|
| 313 |
- for _, path := range valid {
|
|
| 314 |
- if _, err := ValidatePath(path); err != nil {
|
|
| 315 |
- t.Fatalf("ValidatePath(`%q`) should succeed: error %q", path, err)
|
|
| 316 |
- } |
|
| 317 |
- } |
|
| 318 |
- |
|
| 319 |
- for path, expectedError := range invalid {
|
|
| 320 |
- if _, err := ValidatePath(path); err == nil {
|
|
| 321 |
- t.Fatalf("ValidatePath(`%q`) should have failed validation", path)
|
|
| 322 |
- } else {
|
|
| 323 |
- if err.Error() != expectedError {
|
|
| 324 |
- t.Fatalf("ValidatePath(`%q`) error should contain %q, got %q", path, expectedError, err.Error())
|
|
| 325 |
- } |
|
| 326 |
- } |
|
| 327 |
- } |
|
| 328 |
-} |
|
| 329 | 277 |
func TestValidateDevice(t *testing.T) {
|
| 330 | 278 |
valid := []string{
|
| 331 | 279 |
"/home", |
| 332 | 280 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,11 @@ |
| 0 |
+// +build linux freebsd |
|
| 1 |
+ |
|
| 2 |
+package system |
|
| 3 |
+ |
|
| 4 |
+import "syscall" |
|
| 5 |
+ |
|
| 6 |
+// UnmountWithSyscall is a platform-specific helper function to call |
|
| 7 |
+// the unmount syscall. |
|
| 8 |
+func UnmountWithSyscall(dest string) {
|
|
| 9 |
+ syscall.Unmount(dest, 0) |
|
| 10 |
+} |
| ... | ... |
@@ -2,10 +2,12 @@ package runconfig |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 |
+ "fmt" |
|
| 5 | 6 |
"io" |
| 6 | 7 |
|
| 7 | 8 |
"github.com/docker/docker/pkg/nat" |
| 8 | 9 |
"github.com/docker/docker/pkg/stringutils" |
| 10 |
+ "github.com/docker/docker/volume" |
|
| 9 | 11 |
) |
| 10 | 12 |
|
| 11 | 13 |
// Config contains the configuration data about a container. |
| ... | ... |
@@ -44,15 +46,29 @@ type Config struct {
|
| 44 | 44 |
// Be aware this function is not checking whether the resulted structs are nil, |
| 45 | 45 |
// it's your business to do so |
| 46 | 46 |
func DecodeContainerConfig(src io.Reader) (*Config, *HostConfig, error) {
|
| 47 |
- decoder := json.NewDecoder(src) |
|
| 48 |
- |
|
| 49 | 47 |
var w ContainerConfigWrapper |
| 48 |
+ |
|
| 49 |
+ decoder := json.NewDecoder(src) |
|
| 50 | 50 |
if err := decoder.Decode(&w); err != nil {
|
| 51 | 51 |
return nil, nil, err |
| 52 | 52 |
} |
| 53 | 53 |
|
| 54 | 54 |
hc := w.getHostConfig() |
| 55 | 55 |
|
| 56 |
+ // Perform platform-specific processing of Volumes and Binds. |
|
| 57 |
+ if w.Config != nil && hc != nil {
|
|
| 58 |
+ |
|
| 59 |
+ // Initialise the volumes map if currently nil |
|
| 60 |
+ if w.Config.Volumes == nil {
|
|
| 61 |
+ w.Config.Volumes = make(map[string]struct{})
|
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ // Now validate all the volumes and binds |
|
| 65 |
+ if err := validateVolumesAndBindSettings(w.Config, hc); err != nil {
|
|
| 66 |
+ return nil, nil, err |
|
| 67 |
+ } |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 56 | 70 |
// Certain parameters need daemon-side validation that cannot be done |
| 57 | 71 |
// on the client, as only the daemon knows what is valid for the platform. |
| 58 | 72 |
if err := ValidateNetMode(w.Config, hc); err != nil {
|
| ... | ... |
@@ -61,3 +77,22 @@ func DecodeContainerConfig(src io.Reader) (*Config, *HostConfig, error) {
|
| 61 | 61 |
|
| 62 | 62 |
return w.Config, hc, nil |
| 63 | 63 |
} |
| 64 |
+ |
|
| 65 |
+// validateVolumesAndBindSettings validates each of the volumes and bind settings |
|
| 66 |
+// passed by the caller to ensure they are valid. |
|
| 67 |
+func validateVolumesAndBindSettings(c *Config, hc *HostConfig) error {
|
|
| 68 |
+ |
|
| 69 |
+ // Ensure all volumes and binds are valid. |
|
| 70 |
+ for spec := range c.Volumes {
|
|
| 71 |
+ if _, err := volume.ParseMountSpec(spec, hc.VolumeDriver); err != nil {
|
|
| 72 |
+ return fmt.Errorf("Invalid volume spec %q: %v", spec, err)
|
|
| 73 |
+ } |
|
| 74 |
+ } |
|
| 75 |
+ for _, spec := range hc.Binds {
|
|
| 76 |
+ if _, err := volume.ParseMountSpec(spec, hc.VolumeDriver); err != nil {
|
|
| 77 |
+ return fmt.Errorf("Invalid bind mount spec %q: %v", spec, err)
|
|
| 78 |
+ } |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ return nil |
|
| 82 |
+} |
| ... | ... |
@@ -4,19 +4,36 @@ import ( |
| 4 | 4 |
"bytes" |
| 5 | 5 |
"fmt" |
| 6 | 6 |
"io/ioutil" |
| 7 |
+ "runtime" |
|
| 7 | 8 |
"testing" |
| 8 | 9 |
|
| 9 | 10 |
"github.com/docker/docker/pkg/stringutils" |
| 10 | 11 |
) |
| 11 | 12 |
|
| 13 |
+type f struct {
|
|
| 14 |
+ file string |
|
| 15 |
+ entrypoint *stringutils.StrSlice |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 12 | 18 |
func TestDecodeContainerConfig(t *testing.T) {
|
| 13 |
- fixtures := []struct {
|
|
| 14 |
- file string |
|
| 15 |
- entrypoint *stringutils.StrSlice |
|
| 16 |
- }{
|
|
| 17 |
- {"fixtures/container_config_1_14.json", stringutils.NewStrSlice()},
|
|
| 18 |
- {"fixtures/container_config_1_17.json", stringutils.NewStrSlice("bash")},
|
|
| 19 |
- {"fixtures/container_config_1_19.json", stringutils.NewStrSlice("bash")},
|
|
| 19 |
+ |
|
| 20 |
+ var ( |
|
| 21 |
+ fixtures []f |
|
| 22 |
+ image string |
|
| 23 |
+ ) |
|
| 24 |
+ |
|
| 25 |
+ if runtime.GOOS != "windows" {
|
|
| 26 |
+ image = "ubuntu" |
|
| 27 |
+ fixtures = []f{
|
|
| 28 |
+ {"fixtures/unix/container_config_1_14.json", stringutils.NewStrSlice()},
|
|
| 29 |
+ {"fixtures/unix/container_config_1_17.json", stringutils.NewStrSlice("bash")},
|
|
| 30 |
+ {"fixtures/unix/container_config_1_19.json", stringutils.NewStrSlice("bash")},
|
|
| 31 |
+ } |
|
| 32 |
+ } else {
|
|
| 33 |
+ image = "windows" |
|
| 34 |
+ fixtures = []f{
|
|
| 35 |
+ {"fixtures/windows/container_config_1_19.json", stringutils.NewStrSlice("cmd")},
|
|
| 36 |
+ } |
|
| 20 | 37 |
} |
| 21 | 38 |
|
| 22 | 39 |
for _, f := range fixtures {
|
| ... | ... |
@@ -30,15 +47,15 @@ func TestDecodeContainerConfig(t *testing.T) {
|
| 30 | 30 |
t.Fatal(fmt.Errorf("Error parsing %s: %v", f, err))
|
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 |
- if c.Image != "ubuntu" {
|
|
| 34 |
- t.Fatalf("Expected ubuntu image, found %s\n", c.Image)
|
|
| 33 |
+ if c.Image != image {
|
|
| 34 |
+ t.Fatalf("Expected %s image, found %s\n", image, c.Image)
|
|
| 35 | 35 |
} |
| 36 | 36 |
|
| 37 | 37 |
if c.Entrypoint.Len() != f.entrypoint.Len() {
|
| 38 | 38 |
t.Fatalf("Expected %v, found %v\n", f.entrypoint, c.Entrypoint)
|
| 39 | 39 |
} |
| 40 | 40 |
|
| 41 |
- if h.Memory != 1000 {
|
|
| 41 |
+ if h != nil && h.Memory != 1000 {
|
|
| 42 | 42 |
t.Fatalf("Expected memory to be 1000, found %d\n", h.Memory)
|
| 43 | 43 |
} |
| 44 | 44 |
} |
| 45 | 45 |
deleted file mode 100644 |
| ... | ... |
@@ -1,30 +0,0 @@ |
| 1 |
-{
|
|
| 2 |
- "Hostname":"", |
|
| 3 |
- "Domainname": "", |
|
| 4 |
- "User":"", |
|
| 5 |
- "Memory": 1000, |
|
| 6 |
- "MemorySwap":0, |
|
| 7 |
- "CpuShares": 512, |
|
| 8 |
- "Cpuset": "0,1", |
|
| 9 |
- "AttachStdin":false, |
|
| 10 |
- "AttachStdout":true, |
|
| 11 |
- "AttachStderr":true, |
|
| 12 |
- "PortSpecs":null, |
|
| 13 |
- "Tty":false, |
|
| 14 |
- "OpenStdin":false, |
|
| 15 |
- "StdinOnce":false, |
|
| 16 |
- "Env":null, |
|
| 17 |
- "Cmd":[ |
|
| 18 |
- "bash" |
|
| 19 |
- ], |
|
| 20 |
- "Image":"ubuntu", |
|
| 21 |
- "Volumes":{
|
|
| 22 |
- "/tmp": {}
|
|
| 23 |
- }, |
|
| 24 |
- "WorkingDir":"", |
|
| 25 |
- "NetworkDisabled": false, |
|
| 26 |
- "ExposedPorts":{
|
|
| 27 |
- "22/tcp": {}
|
|
| 28 |
- }, |
|
| 29 |
- "RestartPolicy": { "Name": "always" }
|
|
| 30 |
-} |
| 31 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,50 +0,0 @@ |
| 1 |
-{
|
|
| 2 |
- "Hostname": "", |
|
| 3 |
- "Domainname": "", |
|
| 4 |
- "User": "", |
|
| 5 |
- "Memory": 1000, |
|
| 6 |
- "MemorySwap": 0, |
|
| 7 |
- "CpuShares": 512, |
|
| 8 |
- "Cpuset": "0,1", |
|
| 9 |
- "AttachStdin": false, |
|
| 10 |
- "AttachStdout": true, |
|
| 11 |
- "AttachStderr": true, |
|
| 12 |
- "Tty": false, |
|
| 13 |
- "OpenStdin": false, |
|
| 14 |
- "StdinOnce": false, |
|
| 15 |
- "Env": null, |
|
| 16 |
- "Cmd": [ |
|
| 17 |
- "date" |
|
| 18 |
- ], |
|
| 19 |
- "Entrypoint": "bash", |
|
| 20 |
- "Image": "ubuntu", |
|
| 21 |
- "Volumes": {
|
|
| 22 |
- "/tmp": {}
|
|
| 23 |
- }, |
|
| 24 |
- "WorkingDir": "", |
|
| 25 |
- "NetworkDisabled": false, |
|
| 26 |
- "MacAddress": "12:34:56:78:9a:bc", |
|
| 27 |
- "ExposedPorts": {
|
|
| 28 |
- "22/tcp": {}
|
|
| 29 |
- }, |
|
| 30 |
- "SecurityOpt": [""], |
|
| 31 |
- "HostConfig": {
|
|
| 32 |
- "Binds": ["/tmp:/tmp"], |
|
| 33 |
- "Links": ["redis3:redis"], |
|
| 34 |
- "LxcConf": {"lxc.utsname":"docker"},
|
|
| 35 |
- "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] },
|
|
| 36 |
- "PublishAllPorts": false, |
|
| 37 |
- "Privileged": false, |
|
| 38 |
- "ReadonlyRootfs": false, |
|
| 39 |
- "Dns": ["8.8.8.8"], |
|
| 40 |
- "DnsSearch": [""], |
|
| 41 |
- "DnsOptions": [""], |
|
| 42 |
- "ExtraHosts": null, |
|
| 43 |
- "VolumesFrom": ["parent", "other:ro"], |
|
| 44 |
- "CapAdd": ["NET_ADMIN"], |
|
| 45 |
- "CapDrop": ["MKNOD"], |
|
| 46 |
- "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
|
|
| 47 |
- "NetworkMode": "bridge", |
|
| 48 |
- "Devices": [] |
|
| 49 |
- } |
|
| 50 |
-} |
| 51 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,58 +0,0 @@ |
| 1 |
-{
|
|
| 2 |
- "Hostname": "", |
|
| 3 |
- "Domainname": "", |
|
| 4 |
- "User": "", |
|
| 5 |
- "AttachStdin": false, |
|
| 6 |
- "AttachStdout": true, |
|
| 7 |
- "AttachStderr": true, |
|
| 8 |
- "Tty": false, |
|
| 9 |
- "OpenStdin": false, |
|
| 10 |
- "StdinOnce": false, |
|
| 11 |
- "Env": null, |
|
| 12 |
- "Cmd": [ |
|
| 13 |
- "date" |
|
| 14 |
- ], |
|
| 15 |
- "Entrypoint": "bash", |
|
| 16 |
- "Image": "ubuntu", |
|
| 17 |
- "Labels": {
|
|
| 18 |
- "com.example.vendor": "Acme", |
|
| 19 |
- "com.example.license": "GPL", |
|
| 20 |
- "com.example.version": "1.0" |
|
| 21 |
- }, |
|
| 22 |
- "Volumes": {
|
|
| 23 |
- "/tmp": {}
|
|
| 24 |
- }, |
|
| 25 |
- "WorkingDir": "", |
|
| 26 |
- "NetworkDisabled": false, |
|
| 27 |
- "MacAddress": "12:34:56:78:9a:bc", |
|
| 28 |
- "ExposedPorts": {
|
|
| 29 |
- "22/tcp": {}
|
|
| 30 |
- }, |
|
| 31 |
- "HostConfig": {
|
|
| 32 |
- "Binds": ["/tmp:/tmp"], |
|
| 33 |
- "Links": ["redis3:redis"], |
|
| 34 |
- "LxcConf": {"lxc.utsname":"docker"},
|
|
| 35 |
- "Memory": 1000, |
|
| 36 |
- "MemorySwap": 0, |
|
| 37 |
- "CpuShares": 512, |
|
| 38 |
- "CpusetCpus": "0,1", |
|
| 39 |
- "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] },
|
|
| 40 |
- "PublishAllPorts": false, |
|
| 41 |
- "Privileged": false, |
|
| 42 |
- "ReadonlyRootfs": false, |
|
| 43 |
- "Dns": ["8.8.8.8"], |
|
| 44 |
- "DnsSearch": [""], |
|
| 45 |
- "DnsOptions": [""], |
|
| 46 |
- "ExtraHosts": null, |
|
| 47 |
- "VolumesFrom": ["parent", "other:ro"], |
|
| 48 |
- "CapAdd": ["NET_ADMIN"], |
|
| 49 |
- "CapDrop": ["MKNOD"], |
|
| 50 |
- "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
|
|
| 51 |
- "NetworkMode": "bridge", |
|
| 52 |
- "Devices": [], |
|
| 53 |
- "Ulimits": [{}],
|
|
| 54 |
- "LogConfig": { "Type": "json-file", "Config": {} },
|
|
| 55 |
- "SecurityOpt": [""], |
|
| 56 |
- "CgroupParent": "" |
|
| 57 |
- } |
|
| 58 |
-} |
| 59 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,18 +0,0 @@ |
| 1 |
-{
|
|
| 2 |
- "Binds": ["/tmp:/tmp"], |
|
| 3 |
- "ContainerIDFile": "", |
|
| 4 |
- "LxcConf": [], |
|
| 5 |
- "Privileged": false, |
|
| 6 |
- "PortBindings": {
|
|
| 7 |
- "80/tcp": [ |
|
| 8 |
- {
|
|
| 9 |
- "HostIp": "0.0.0.0", |
|
| 10 |
- "HostPort": "49153" |
|
| 11 |
- } |
|
| 12 |
- ] |
|
| 13 |
- }, |
|
| 14 |
- "Links": ["/name:alias"], |
|
| 15 |
- "PublishAllPorts": false, |
|
| 16 |
- "CapAdd": ["NET_ADMIN"], |
|
| 17 |
- "CapDrop": ["MKNOD"] |
|
| 18 |
-} |
| 19 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,30 +0,0 @@ |
| 1 |
-{
|
|
| 2 |
- "Binds": ["/tmp:/tmp"], |
|
| 3 |
- "Links": ["redis3:redis"], |
|
| 4 |
- "LxcConf": {"lxc.utsname":"docker"},
|
|
| 5 |
- "Memory": 0, |
|
| 6 |
- "MemorySwap": 0, |
|
| 7 |
- "CpuShares": 512, |
|
| 8 |
- "CpuPeriod": 100000, |
|
| 9 |
- "CpusetCpus": "0,1", |
|
| 10 |
- "CpusetMems": "0,1", |
|
| 11 |
- "BlkioWeight": 300, |
|
| 12 |
- "OomKillDisable": false, |
|
| 13 |
- "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] },
|
|
| 14 |
- "PublishAllPorts": false, |
|
| 15 |
- "Privileged": false, |
|
| 16 |
- "ReadonlyRootfs": false, |
|
| 17 |
- "Dns": ["8.8.8.8"], |
|
| 18 |
- "DnsSearch": [""], |
|
| 19 |
- "ExtraHosts": null, |
|
| 20 |
- "VolumesFrom": ["parent", "other:ro"], |
|
| 21 |
- "CapAdd": ["NET_ADMIN"], |
|
| 22 |
- "CapDrop": ["MKNOD"], |
|
| 23 |
- "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
|
|
| 24 |
- "NetworkMode": "bridge", |
|
| 25 |
- "Devices": [], |
|
| 26 |
- "Ulimits": [{}],
|
|
| 27 |
- "LogConfig": { "Type": "json-file", "Config": {} },
|
|
| 28 |
- "SecurityOpt": [""], |
|
| 29 |
- "CgroupParent": "" |
|
| 30 |
-} |
| 31 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,30 @@ |
| 0 |
+{
|
|
| 1 |
+ "Hostname":"", |
|
| 2 |
+ "Domainname": "", |
|
| 3 |
+ "User":"", |
|
| 4 |
+ "Memory": 1000, |
|
| 5 |
+ "MemorySwap":0, |
|
| 6 |
+ "CpuShares": 512, |
|
| 7 |
+ "Cpuset": "0,1", |
|
| 8 |
+ "AttachStdin":false, |
|
| 9 |
+ "AttachStdout":true, |
|
| 10 |
+ "AttachStderr":true, |
|
| 11 |
+ "PortSpecs":null, |
|
| 12 |
+ "Tty":false, |
|
| 13 |
+ "OpenStdin":false, |
|
| 14 |
+ "StdinOnce":false, |
|
| 15 |
+ "Env":null, |
|
| 16 |
+ "Cmd":[ |
|
| 17 |
+ "bash" |
|
| 18 |
+ ], |
|
| 19 |
+ "Image":"ubuntu", |
|
| 20 |
+ "Volumes":{
|
|
| 21 |
+ "/tmp": {}
|
|
| 22 |
+ }, |
|
| 23 |
+ "WorkingDir":"", |
|
| 24 |
+ "NetworkDisabled": false, |
|
| 25 |
+ "ExposedPorts":{
|
|
| 26 |
+ "22/tcp": {}
|
|
| 27 |
+ }, |
|
| 28 |
+ "RestartPolicy": { "Name": "always" }
|
|
| 29 |
+} |
| 0 | 30 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,50 @@ |
| 0 |
+{
|
|
| 1 |
+ "Hostname": "", |
|
| 2 |
+ "Domainname": "", |
|
| 3 |
+ "User": "", |
|
| 4 |
+ "Memory": 1000, |
|
| 5 |
+ "MemorySwap": 0, |
|
| 6 |
+ "CpuShares": 512, |
|
| 7 |
+ "Cpuset": "0,1", |
|
| 8 |
+ "AttachStdin": false, |
|
| 9 |
+ "AttachStdout": true, |
|
| 10 |
+ "AttachStderr": true, |
|
| 11 |
+ "Tty": false, |
|
| 12 |
+ "OpenStdin": false, |
|
| 13 |
+ "StdinOnce": false, |
|
| 14 |
+ "Env": null, |
|
| 15 |
+ "Cmd": [ |
|
| 16 |
+ "date" |
|
| 17 |
+ ], |
|
| 18 |
+ "Entrypoint": "bash", |
|
| 19 |
+ "Image": "ubuntu", |
|
| 20 |
+ "Volumes": {
|
|
| 21 |
+ "/tmp": {}
|
|
| 22 |
+ }, |
|
| 23 |
+ "WorkingDir": "", |
|
| 24 |
+ "NetworkDisabled": false, |
|
| 25 |
+ "MacAddress": "12:34:56:78:9a:bc", |
|
| 26 |
+ "ExposedPorts": {
|
|
| 27 |
+ "22/tcp": {}
|
|
| 28 |
+ }, |
|
| 29 |
+ "SecurityOpt": [""], |
|
| 30 |
+ "HostConfig": {
|
|
| 31 |
+ "Binds": ["/tmp:/tmp"], |
|
| 32 |
+ "Links": ["redis3:redis"], |
|
| 33 |
+ "LxcConf": {"lxc.utsname":"docker"},
|
|
| 34 |
+ "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] },
|
|
| 35 |
+ "PublishAllPorts": false, |
|
| 36 |
+ "Privileged": false, |
|
| 37 |
+ "ReadonlyRootfs": false, |
|
| 38 |
+ "Dns": ["8.8.8.8"], |
|
| 39 |
+ "DnsSearch": [""], |
|
| 40 |
+ "DnsOptions": [""], |
|
| 41 |
+ "ExtraHosts": null, |
|
| 42 |
+ "VolumesFrom": ["parent", "other:ro"], |
|
| 43 |
+ "CapAdd": ["NET_ADMIN"], |
|
| 44 |
+ "CapDrop": ["MKNOD"], |
|
| 45 |
+ "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
|
|
| 46 |
+ "NetworkMode": "bridge", |
|
| 47 |
+ "Devices": [] |
|
| 48 |
+ } |
|
| 49 |
+} |
| 0 | 50 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,58 @@ |
| 0 |
+{
|
|
| 1 |
+ "Hostname": "", |
|
| 2 |
+ "Domainname": "", |
|
| 3 |
+ "User": "", |
|
| 4 |
+ "AttachStdin": false, |
|
| 5 |
+ "AttachStdout": true, |
|
| 6 |
+ "AttachStderr": true, |
|
| 7 |
+ "Tty": false, |
|
| 8 |
+ "OpenStdin": false, |
|
| 9 |
+ "StdinOnce": false, |
|
| 10 |
+ "Env": null, |
|
| 11 |
+ "Cmd": [ |
|
| 12 |
+ "date" |
|
| 13 |
+ ], |
|
| 14 |
+ "Entrypoint": "bash", |
|
| 15 |
+ "Image": "ubuntu", |
|
| 16 |
+ "Labels": {
|
|
| 17 |
+ "com.example.vendor": "Acme", |
|
| 18 |
+ "com.example.license": "GPL", |
|
| 19 |
+ "com.example.version": "1.0" |
|
| 20 |
+ }, |
|
| 21 |
+ "Volumes": {
|
|
| 22 |
+ "/tmp": {}
|
|
| 23 |
+ }, |
|
| 24 |
+ "WorkingDir": "", |
|
| 25 |
+ "NetworkDisabled": false, |
|
| 26 |
+ "MacAddress": "12:34:56:78:9a:bc", |
|
| 27 |
+ "ExposedPorts": {
|
|
| 28 |
+ "22/tcp": {}
|
|
| 29 |
+ }, |
|
| 30 |
+ "HostConfig": {
|
|
| 31 |
+ "Binds": ["/tmp:/tmp"], |
|
| 32 |
+ "Links": ["redis3:redis"], |
|
| 33 |
+ "LxcConf": {"lxc.utsname":"docker"},
|
|
| 34 |
+ "Memory": 1000, |
|
| 35 |
+ "MemorySwap": 0, |
|
| 36 |
+ "CpuShares": 512, |
|
| 37 |
+ "CpusetCpus": "0,1", |
|
| 38 |
+ "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] },
|
|
| 39 |
+ "PublishAllPorts": false, |
|
| 40 |
+ "Privileged": false, |
|
| 41 |
+ "ReadonlyRootfs": false, |
|
| 42 |
+ "Dns": ["8.8.8.8"], |
|
| 43 |
+ "DnsSearch": [""], |
|
| 44 |
+ "DnsOptions": [""], |
|
| 45 |
+ "ExtraHosts": null, |
|
| 46 |
+ "VolumesFrom": ["parent", "other:ro"], |
|
| 47 |
+ "CapAdd": ["NET_ADMIN"], |
|
| 48 |
+ "CapDrop": ["MKNOD"], |
|
| 49 |
+ "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
|
|
| 50 |
+ "NetworkMode": "bridge", |
|
| 51 |
+ "Devices": [], |
|
| 52 |
+ "Ulimits": [{}],
|
|
| 53 |
+ "LogConfig": { "Type": "json-file", "Config": {} },
|
|
| 54 |
+ "SecurityOpt": [""], |
|
| 55 |
+ "CgroupParent": "" |
|
| 56 |
+ } |
|
| 57 |
+} |
| 0 | 58 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,18 @@ |
| 0 |
+{
|
|
| 1 |
+ "Binds": ["/tmp:/tmp"], |
|
| 2 |
+ "ContainerIDFile": "", |
|
| 3 |
+ "LxcConf": [], |
|
| 4 |
+ "Privileged": false, |
|
| 5 |
+ "PortBindings": {
|
|
| 6 |
+ "80/tcp": [ |
|
| 7 |
+ {
|
|
| 8 |
+ "HostIp": "0.0.0.0", |
|
| 9 |
+ "HostPort": "49153" |
|
| 10 |
+ } |
|
| 11 |
+ ] |
|
| 12 |
+ }, |
|
| 13 |
+ "Links": ["/name:alias"], |
|
| 14 |
+ "PublishAllPorts": false, |
|
| 15 |
+ "CapAdd": ["NET_ADMIN"], |
|
| 16 |
+ "CapDrop": ["MKNOD"] |
|
| 17 |
+} |
| 0 | 18 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,30 @@ |
| 0 |
+{
|
|
| 1 |
+ "Binds": ["/tmp:/tmp"], |
|
| 2 |
+ "Links": ["redis3:redis"], |
|
| 3 |
+ "LxcConf": {"lxc.utsname":"docker"},
|
|
| 4 |
+ "Memory": 0, |
|
| 5 |
+ "MemorySwap": 0, |
|
| 6 |
+ "CpuShares": 512, |
|
| 7 |
+ "CpuPeriod": 100000, |
|
| 8 |
+ "CpusetCpus": "0,1", |
|
| 9 |
+ "CpusetMems": "0,1", |
|
| 10 |
+ "BlkioWeight": 300, |
|
| 11 |
+ "OomKillDisable": false, |
|
| 12 |
+ "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] },
|
|
| 13 |
+ "PublishAllPorts": false, |
|
| 14 |
+ "Privileged": false, |
|
| 15 |
+ "ReadonlyRootfs": false, |
|
| 16 |
+ "Dns": ["8.8.8.8"], |
|
| 17 |
+ "DnsSearch": [""], |
|
| 18 |
+ "ExtraHosts": null, |
|
| 19 |
+ "VolumesFrom": ["parent", "other:ro"], |
|
| 20 |
+ "CapAdd": ["NET_ADMIN"], |
|
| 21 |
+ "CapDrop": ["MKNOD"], |
|
| 22 |
+ "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
|
|
| 23 |
+ "NetworkMode": "bridge", |
|
| 24 |
+ "Devices": [], |
|
| 25 |
+ "Ulimits": [{}],
|
|
| 26 |
+ "LogConfig": { "Type": "json-file", "Config": {} },
|
|
| 27 |
+ "SecurityOpt": [""], |
|
| 28 |
+ "CgroupParent": "" |
|
| 29 |
+} |
| 0 | 30 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,58 @@ |
| 0 |
+{
|
|
| 1 |
+ "Hostname": "", |
|
| 2 |
+ "Domainname": "", |
|
| 3 |
+ "User": "", |
|
| 4 |
+ "AttachStdin": false, |
|
| 5 |
+ "AttachStdout": true, |
|
| 6 |
+ "AttachStderr": true, |
|
| 7 |
+ "Tty": false, |
|
| 8 |
+ "OpenStdin": false, |
|
| 9 |
+ "StdinOnce": false, |
|
| 10 |
+ "Env": null, |
|
| 11 |
+ "Cmd": [ |
|
| 12 |
+ "date" |
|
| 13 |
+ ], |
|
| 14 |
+ "Entrypoint": "cmd", |
|
| 15 |
+ "Image": "windows", |
|
| 16 |
+ "Labels": {
|
|
| 17 |
+ "com.example.vendor": "Acme", |
|
| 18 |
+ "com.example.license": "GPL", |
|
| 19 |
+ "com.example.version": "1.0" |
|
| 20 |
+ }, |
|
| 21 |
+ "Volumes": {
|
|
| 22 |
+ "c:/windows": {}
|
|
| 23 |
+ }, |
|
| 24 |
+ "WorkingDir": "", |
|
| 25 |
+ "NetworkDisabled": false, |
|
| 26 |
+ "MacAddress": "12:34:56:78:9a:bc", |
|
| 27 |
+ "ExposedPorts": {
|
|
| 28 |
+ "22/tcp": {}
|
|
| 29 |
+ }, |
|
| 30 |
+ "HostConfig": {
|
|
| 31 |
+ "Binds": ["c:/windows:d:/tmp"], |
|
| 32 |
+ "Links": ["redis3:redis"], |
|
| 33 |
+ "LxcConf": {"lxc.utsname":"docker"},
|
|
| 34 |
+ "Memory": 1000, |
|
| 35 |
+ "MemorySwap": 0, |
|
| 36 |
+ "CpuShares": 512, |
|
| 37 |
+ "CpusetCpus": "0,1", |
|
| 38 |
+ "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] },
|
|
| 39 |
+ "PublishAllPorts": false, |
|
| 40 |
+ "Privileged": false, |
|
| 41 |
+ "ReadonlyRootfs": false, |
|
| 42 |
+ "Dns": ["8.8.8.8"], |
|
| 43 |
+ "DnsSearch": [""], |
|
| 44 |
+ "DnsOptions": [""], |
|
| 45 |
+ "ExtraHosts": null, |
|
| 46 |
+ "VolumesFrom": ["parent", "other:ro"], |
|
| 47 |
+ "CapAdd": ["NET_ADMIN"], |
|
| 48 |
+ "CapDrop": ["MKNOD"], |
|
| 49 |
+ "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
|
|
| 50 |
+ "NetworkMode": "default", |
|
| 51 |
+ "Devices": [], |
|
| 52 |
+ "Ulimits": [{}],
|
|
| 53 |
+ "LogConfig": { "Type": "json-file", "Config": {} },
|
|
| 54 |
+ "SecurityOpt": [""], |
|
| 55 |
+ "CgroupParent": "" |
|
| 56 |
+ } |
|
| 57 |
+} |
| ... | ... |
@@ -234,8 +234,8 @@ func TestDecodeHostConfig(t *testing.T) {
|
| 234 | 234 |
fixtures := []struct {
|
| 235 | 235 |
file string |
| 236 | 236 |
}{
|
| 237 |
- {"fixtures/container_hostconfig_1_14.json"},
|
|
| 238 |
- {"fixtures/container_hostconfig_1_19.json"},
|
|
| 237 |
+ {"fixtures/unix/container_hostconfig_1_14.json"},
|
|
| 238 |
+ {"fixtures/unix/container_hostconfig_1_19.json"},
|
|
| 239 | 239 |
} |
| 240 | 240 |
|
| 241 | 241 |
for _, f := range fixtures {
|
| ... | ... |
@@ -12,6 +12,7 @@ import ( |
| 12 | 12 |
"github.com/docker/docker/pkg/signal" |
| 13 | 13 |
"github.com/docker/docker/pkg/stringutils" |
| 14 | 14 |
"github.com/docker/docker/pkg/units" |
| 15 |
+ "github.com/docker/docker/volume" |
|
| 15 | 16 |
) |
| 16 | 17 |
|
| 17 | 18 |
var ( |
| ... | ... |
@@ -46,7 +47,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe |
| 46 | 46 |
var ( |
| 47 | 47 |
// FIXME: use utils.ListOpts for attach and volumes? |
| 48 | 48 |
flAttach = opts.NewListOpts(opts.ValidateAttach) |
| 49 |
- flVolumes = opts.NewListOpts(opts.ValidatePath) |
|
| 49 |
+ flVolumes = opts.NewListOpts(nil) |
|
| 50 | 50 |
flLinks = opts.NewListOpts(opts.ValidateLink) |
| 51 | 51 |
flEnv = opts.NewListOpts(opts.ValidateEnv) |
| 52 | 52 |
flLabels = opts.NewListOpts(opts.ValidateEnv) |
| ... | ... |
@@ -201,16 +202,11 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe |
| 201 | 201 |
var binds []string |
| 202 | 202 |
// add any bind targets to the list of container volumes |
| 203 | 203 |
for bind := range flVolumes.GetMap() {
|
| 204 |
- if arr := strings.Split(bind, ":"); len(arr) > 1 {
|
|
| 205 |
- if arr[1] == "/" {
|
|
| 206 |
- return nil, nil, cmd, fmt.Errorf("Invalid bind mount: destination can't be '/'")
|
|
| 207 |
- } |
|
| 204 |
+ if arr := volume.SplitN(bind, 2); len(arr) > 1 {
|
|
| 208 | 205 |
// after creating the bind mount we want to delete it from the flVolumes values because |
| 209 | 206 |
// we do not want bind mounts being committed to image configs |
| 210 | 207 |
binds = append(binds, bind) |
| 211 | 208 |
flVolumes.Delete(bind) |
| 212 |
- } else if bind == "/" {
|
|
| 213 |
- return nil, nil, cmd, fmt.Errorf("Invalid volume: path can't be '/'")
|
|
| 214 | 209 |
} |
| 215 | 210 |
} |
| 216 | 211 |
|
| ... | ... |
@@ -1,8 +1,12 @@ |
| 1 | 1 |
package runconfig |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "bytes" |
|
| 5 |
+ "encoding/json" |
|
| 4 | 6 |
"fmt" |
| 5 | 7 |
"io/ioutil" |
| 8 |
+ "os" |
|
| 9 |
+ "runtime" |
|
| 6 | 10 |
"strings" |
| 7 | 11 |
"testing" |
| 8 | 12 |
|
| ... | ... |
@@ -31,17 +35,6 @@ func mustParse(t *testing.T, args string) (*Config, *HostConfig) {
|
| 31 | 31 |
return config, hostConfig |
| 32 | 32 |
} |
| 33 | 33 |
|
| 34 |
-// check if (a == c && b == d) || (a == d && b == c) |
|
| 35 |
-// because maps are randomized |
|
| 36 |
-func compareRandomizedStrings(a, b, c, d string) error {
|
|
| 37 |
- if a == c && b == d {
|
|
| 38 |
- return nil |
|
| 39 |
- } |
|
| 40 |
- if a == d && b == c {
|
|
| 41 |
- return nil |
|
| 42 |
- } |
|
| 43 |
- return fmt.Errorf("strings don't match")
|
|
| 44 |
-} |
|
| 45 | 34 |
func TestParseRunLinks(t *testing.T) {
|
| 46 | 35 |
if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
|
| 47 | 36 |
t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
|
| ... | ... |
@@ -98,81 +91,257 @@ func TestParseRunAttach(t *testing.T) {
|
| 98 | 98 |
} |
| 99 | 99 |
|
| 100 | 100 |
func TestParseRunVolumes(t *testing.T) {
|
| 101 |
- if config, hostConfig := mustParse(t, "-v /tmp"); hostConfig.Binds != nil {
|
|
| 102 |
- t.Fatalf("Error parsing volume flags, `-v /tmp` should not mount-bind anything. Received %v", hostConfig.Binds)
|
|
| 103 |
- } else if _, exists := config.Volumes["/tmp"]; !exists {
|
|
| 104 |
- t.Fatalf("Error parsing volume flags, `-v /tmp` is missing from volumes. Received %v", config.Volumes)
|
|
| 101 |
+ |
|
| 102 |
+ // A single volume |
|
| 103 |
+ arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
|
| 104 |
+ if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
|
|
| 105 |
+ t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
|
|
| 106 |
+ } else if _, exists := config.Volumes[arr[0]]; !exists {
|
|
| 107 |
+ t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
|
|
| 105 | 108 |
} |
| 106 | 109 |
|
| 107 |
- if config, hostConfig := mustParse(t, "-v /tmp -v /var"); hostConfig.Binds != nil {
|
|
| 108 |
- t.Fatalf("Error parsing volume flags, `-v /tmp -v /var` should not mount-bind anything. Received %v", hostConfig.Binds)
|
|
| 109 |
- } else if _, exists := config.Volumes["/tmp"]; !exists {
|
|
| 110 |
- t.Fatalf("Error parsing volume flags, `-v /tmp` is missing from volumes. Received %v", config.Volumes)
|
|
| 111 |
- } else if _, exists := config.Volumes["/var"]; !exists {
|
|
| 112 |
- t.Fatalf("Error parsing volume flags, `-v /var` is missing from volumes. Received %v", config.Volumes)
|
|
| 110 |
+ // Two volumes |
|
| 111 |
+ arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
|
|
| 112 |
+ if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
|
|
| 113 |
+ t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
|
|
| 114 |
+ } else if _, exists := config.Volumes[arr[0]]; !exists {
|
|
| 115 |
+ t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
|
|
| 116 |
+ } else if _, exists := config.Volumes[arr[1]]; !exists {
|
|
| 117 |
+ t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
|
|
| 113 | 118 |
} |
| 114 | 119 |
|
| 115 |
- if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp"); hostConfig.Binds == nil || hostConfig.Binds[0] != "/hostTmp:/containerTmp" {
|
|
| 116 |
- t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp` should mount-bind /hostTmp into /containerTmp. Received %v", hostConfig.Binds)
|
|
| 120 |
+ // A single bind-mount |
|
| 121 |
+ arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
|
|
| 122 |
+ if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
|
|
| 123 |
+ t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
|
|
| 117 | 124 |
} |
| 118 | 125 |
|
| 119 |
- if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp -v /hostVar:/containerVar"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/hostTmp:/containerTmp", "/hostVar:/containerVar") != nil {
|
|
| 120 |
- t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp -v /hostVar:/containerVar` should mount-bind /hostTmp into /containerTmp and /hostVar into /hostContainer. Received %v", hostConfig.Binds)
|
|
| 126 |
+ // Two bind-mounts. |
|
| 127 |
+ arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
|
|
| 128 |
+ if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
|
| 129 |
+ t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
|
| 121 | 130 |
} |
| 122 | 131 |
|
| 123 |
- if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp:ro -v /hostVar:/containerVar:rw"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/hostTmp:/containerTmp:ro", "/hostVar:/containerVar:rw") != nil {
|
|
| 124 |
- t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp:ro -v /hostVar:/containerVar:rw` should mount-bind /hostTmp into /containerTmp and /hostVar into /hostContainer. Received %v", hostConfig.Binds)
|
|
| 132 |
+ // Two bind-mounts, first read-only, second read-write. |
|
| 133 |
+ // TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4 |
|
| 134 |
+ arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`})
|
|
| 135 |
+ if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
|
| 136 |
+ t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
|
| 125 | 137 |
} |
| 126 | 138 |
|
| 127 |
- if _, hostConfig := mustParse(t, "-v /containerTmp:ro -v /containerVar:rw"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/containerTmp:ro", "/containerVar:rw") != nil {
|
|
| 128 |
- t.Fatalf("Error parsing volume flags, `-v /containerTmp:ro -v /containerVar:rw` should mount-bind /containerTmp into /ro and /containerVar into /rw. Received %v", hostConfig.Binds)
|
|
| 139 |
+ // Similar to previous test but with alternate modes which are only supported by Linux |
|
| 140 |
+ if runtime.GOOS != "windows" {
|
|
| 141 |
+ arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
|
|
| 142 |
+ if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
|
| 143 |
+ t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
|
| 144 |
+ } |
|
| 145 |
+ |
|
| 146 |
+ arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
|
|
| 147 |
+ if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
|
| 148 |
+ t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
|
| 149 |
+ } |
|
| 129 | 150 |
} |
| 130 | 151 |
|
| 131 |
- if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp:ro,Z -v /hostVar:/containerVar:rw,Z"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/hostTmp:/containerTmp:ro,Z", "/hostVar:/containerVar:rw,Z") != nil {
|
|
| 132 |
- t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp:ro,Z -v /hostVar:/containerVar:rw,Z` should mount-bind /hostTmp into /containerTmp and /hostVar into /hostContainer. Received %v", hostConfig.Binds)
|
|
| 152 |
+ // One bind mount and one volume |
|
| 153 |
+ arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
|
|
| 154 |
+ if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
|
|
| 155 |
+ t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds)
|
|
| 156 |
+ } else if _, exists := config.Volumes[arr[1]]; !exists {
|
|
| 157 |
+ t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
|
|
| 133 | 158 |
} |
| 134 | 159 |
|
| 135 |
- if _, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp:Z -v /hostVar:/containerVar:z"); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], "/hostTmp:/containerTmp:Z", "/hostVar:/containerVar:z") != nil {
|
|
| 136 |
- t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp:Z -v /hostVar:/containerVar:z` should mount-bind /hostTmp into /containerTmp and /hostVar into /hostContainer. Received %v", hostConfig.Binds)
|
|
| 160 |
+ // Root to non-c: drive letter (Windows specific) |
|
| 161 |
+ if runtime.GOOS == "windows" {
|
|
| 162 |
+ arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
|
|
| 163 |
+ if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
|
|
| 164 |
+ t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
|
|
| 165 |
+ } |
|
| 137 | 166 |
} |
| 138 | 167 |
|
| 139 |
- if config, hostConfig := mustParse(t, "-v /hostTmp:/containerTmp -v /containerVar"); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != "/hostTmp:/containerTmp" {
|
|
| 140 |
- t.Fatalf("Error parsing volume flags, `-v /hostTmp:/containerTmp -v /containerVar` should mount-bind only /hostTmp into /containerTmp. Received %v", hostConfig.Binds)
|
|
| 141 |
- } else if _, exists := config.Volumes["/containerVar"]; !exists {
|
|
| 142 |
- t.Fatalf("Error parsing volume flags, `-v /containerVar` is missing from volumes. Received %v", config.Volumes)
|
|
| 168 |
+} |
|
| 169 |
+ |
|
| 170 |
+// This tests the cases for binds which are generated through |
|
| 171 |
+// DecodeContainerConfig rather than Parse() |
|
| 172 |
+func TestDecodeContainerConfigVolumes(t *testing.T) {
|
|
| 173 |
+ |
|
| 174 |
+ // Root to root |
|
| 175 |
+ bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`})
|
|
| 176 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 177 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 178 |
+ } |
|
| 179 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 180 |
+ t.Fatalf("volume %v should have failed", bindsOrVols)
|
|
| 143 | 181 |
} |
| 144 | 182 |
|
| 145 |
- if config, hostConfig := mustParse(t, ""); hostConfig.Binds != nil {
|
|
| 146 |
- t.Fatalf("Error parsing volume flags, without volume, nothing should be mount-binded. Received %v", hostConfig.Binds)
|
|
| 147 |
- } else if len(config.Volumes) != 0 {
|
|
| 148 |
- t.Fatalf("Error parsing volume flags, without volume, no volume should be present. Received %v", config.Volumes)
|
|
| 183 |
+ // No destination path |
|
| 184 |
+ bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`})
|
|
| 185 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 186 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 187 |
+ } |
|
| 188 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 189 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 149 | 190 |
} |
| 150 | 191 |
|
| 151 |
- if _, _, err := parse(t, "-v /"); err == nil {
|
|
| 152 |
- t.Fatalf("Expected error, but got none")
|
|
| 192 |
+ // // No destination path or mode |
|
| 193 |
+ bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`})
|
|
| 194 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 195 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 196 |
+ } |
|
| 197 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 198 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 153 | 199 |
} |
| 154 | 200 |
|
| 155 |
- if _, _, err := parse(t, "-v /:/"); err == nil {
|
|
| 156 |
- t.Fatalf("Error parsing volume flags, `-v /:/` should fail but didn't")
|
|
| 201 |
+ // A whole lot of nothing |
|
| 202 |
+ bindsOrVols = []string{`:`}
|
|
| 203 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 204 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 157 | 205 |
} |
| 158 |
- if _, _, err := parse(t, "-v"); err == nil {
|
|
| 159 |
- t.Fatalf("Error parsing volume flags, `-v` should fail but didn't")
|
|
| 206 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 207 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 160 | 208 |
} |
| 161 |
- if _, _, err := parse(t, "-v /tmp:"); err == nil {
|
|
| 162 |
- t.Fatalf("Error parsing volume flags, `-v /tmp:` should fail but didn't")
|
|
| 209 |
+ |
|
| 210 |
+ // A whole lot of nothing with no mode |
|
| 211 |
+ bindsOrVols = []string{`::`}
|
|
| 212 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 213 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 163 | 214 |
} |
| 164 |
- if _, _, err := parse(t, "-v /tmp::"); err == nil {
|
|
| 165 |
- t.Fatalf("Error parsing volume flags, `-v /tmp::` should fail but didn't")
|
|
| 215 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 216 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 166 | 217 |
} |
| 167 |
- if _, _, err := parse(t, "-v :"); err == nil {
|
|
| 168 |
- t.Fatalf("Error parsing volume flags, `-v :` should fail but didn't")
|
|
| 218 |
+ |
|
| 219 |
+ // Too much including an invalid mode |
|
| 220 |
+ wTmp := os.Getenv("TEMP")
|
|
| 221 |
+ bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp})
|
|
| 222 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 223 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 169 | 224 |
} |
| 170 |
- if _, _, err := parse(t, "-v ::"); err == nil {
|
|
| 171 |
- t.Fatalf("Error parsing volume flags, `-v ::` should fail but didn't")
|
|
| 225 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 226 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 172 | 227 |
} |
| 173 |
- if _, _, err := parse(t, "-v /tmp:/tmp:/tmp:/tmp"); err == nil {
|
|
| 174 |
- t.Fatalf("Error parsing volume flags, `-v /tmp:/tmp:/tmp:/tmp` should fail but didn't")
|
|
| 228 |
+ |
|
| 229 |
+ // Windows specific error tests |
|
| 230 |
+ if runtime.GOOS == "windows" {
|
|
| 231 |
+ // Volume which does not include a drive letter |
|
| 232 |
+ bindsOrVols = []string{`\tmp`}
|
|
| 233 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 234 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 235 |
+ } |
|
| 236 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 237 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 238 |
+ } |
|
| 239 |
+ |
|
| 240 |
+ // Root to C-Drive |
|
| 241 |
+ bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`}
|
|
| 242 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 243 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 244 |
+ } |
|
| 245 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 246 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 247 |
+ } |
|
| 248 |
+ |
|
| 249 |
+ // Container path that does not include a drive letter |
|
| 250 |
+ bindsOrVols = []string{`c:\windows:\somewhere`}
|
|
| 251 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 252 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 253 |
+ } |
|
| 254 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 255 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 256 |
+ } |
|
| 175 | 257 |
} |
| 258 |
+ |
|
| 259 |
+ // Linux-specific error tests |
|
| 260 |
+ if runtime.GOOS != "windows" {
|
|
| 261 |
+ // Just root |
|
| 262 |
+ bindsOrVols = []string{`/`}
|
|
| 263 |
+ if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
|
| 264 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 265 |
+ } |
|
| 266 |
+ if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
|
| 267 |
+ t.Fatalf("binds %v should have failed", bindsOrVols)
|
|
| 268 |
+ } |
|
| 269 |
+ |
|
| 270 |
+ // A single volume that looks like a bind mount passed in Volumes. |
|
| 271 |
+ // This should be handled as a bind mount, not a volume. |
|
| 272 |
+ vols := []string{`/foo:/bar`}
|
|
| 273 |
+ if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil {
|
|
| 274 |
+ t.Fatal("Volume /foo:/bar should have succeeded as a volume name")
|
|
| 275 |
+ } else if hostConfig.Binds != nil {
|
|
| 276 |
+ t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds)
|
|
| 277 |
+ } else if _, exists := config.Volumes[vols[0]]; !exists {
|
|
| 278 |
+ t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes)
|
|
| 279 |
+ } |
|
| 280 |
+ |
|
| 281 |
+ } |
|
| 282 |
+} |
|
| 283 |
+ |
|
| 284 |
+// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes |
|
| 285 |
+// to call DecodeContainerConfig. It effectively does what a client would |
|
| 286 |
+// do when calling the daemon by constructing a JSON stream of a |
|
| 287 |
+// ContainerConfigWrapper which is populated by the set of volume specs |
|
| 288 |
+// passed into it. It returns a config and a hostconfig which can be |
|
| 289 |
+// validated to ensure DecodeContainerConfig has manipulated the structures |
|
| 290 |
+// correctly. |
|
| 291 |
+func callDecodeContainerConfig(volumes []string, binds []string) (*Config, *HostConfig, error) {
|
|
| 292 |
+ var ( |
|
| 293 |
+ b []byte |
|
| 294 |
+ err error |
|
| 295 |
+ c *Config |
|
| 296 |
+ h *HostConfig |
|
| 297 |
+ ) |
|
| 298 |
+ w := ContainerConfigWrapper{
|
|
| 299 |
+ Config: &Config{
|
|
| 300 |
+ Volumes: map[string]struct{}{},
|
|
| 301 |
+ }, |
|
| 302 |
+ HostConfig: &HostConfig{
|
|
| 303 |
+ NetworkMode: "none", |
|
| 304 |
+ Binds: binds, |
|
| 305 |
+ }, |
|
| 306 |
+ } |
|
| 307 |
+ for _, v := range volumes {
|
|
| 308 |
+ w.Config.Volumes[v] = struct{}{}
|
|
| 309 |
+ } |
|
| 310 |
+ if b, err = json.Marshal(w); err != nil {
|
|
| 311 |
+ return nil, nil, fmt.Errorf("Error on marshal %s", err.Error())
|
|
| 312 |
+ } |
|
| 313 |
+ c, h, err = DecodeContainerConfig(bytes.NewReader(b)) |
|
| 314 |
+ if err != nil {
|
|
| 315 |
+ return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err)
|
|
| 316 |
+ } |
|
| 317 |
+ if c == nil || h == nil {
|
|
| 318 |
+ return nil, nil, fmt.Errorf("Empty config or hostconfig")
|
|
| 319 |
+ } |
|
| 320 |
+ |
|
| 321 |
+ return c, h, err |
|
| 322 |
+} |
|
| 323 |
+ |
|
| 324 |
+// check if (a == c && b == d) || (a == d && b == c) |
|
| 325 |
+// because maps are randomized |
|
| 326 |
+func compareRandomizedStrings(a, b, c, d string) error {
|
|
| 327 |
+ if a == c && b == d {
|
|
| 328 |
+ return nil |
|
| 329 |
+ } |
|
| 330 |
+ if a == d && b == c {
|
|
| 331 |
+ return nil |
|
| 332 |
+ } |
|
| 333 |
+ return fmt.Errorf("strings don't match")
|
|
| 334 |
+} |
|
| 335 |
+ |
|
| 336 |
+// setupPlatformVolume takes two arrays of volume specs - a Unix style |
|
| 337 |
+// spec and a Windows style spec. Depending on the platform being unit tested, |
|
| 338 |
+// it returns one of them, along with a volume string that would be passed |
|
| 339 |
+// on the docker CLI (eg -v /bar -v /foo). |
|
| 340 |
+func setupPlatformVolume(u []string, w []string) ([]string, string) {
|
|
| 341 |
+ var a []string |
|
| 342 |
+ if runtime.GOOS == "windows" {
|
|
| 343 |
+ a = w |
|
| 344 |
+ } else {
|
|
| 345 |
+ a = u |
|
| 346 |
+ } |
|
| 347 |
+ s := "" |
|
| 348 |
+ for _, v := range a {
|
|
| 349 |
+ s = s + "-v " + v + " " |
|
| 350 |
+ } |
|
| 351 |
+ return a, s |
|
| 176 | 352 |
} |
| 177 | 353 |
|
| 178 | 354 |
func TestParseLxcConfOpt(t *testing.T) {
|
| ... | ... |
@@ -438,9 +607,13 @@ func TestParseLoggingOpts(t *testing.T) {
|
| 438 | 438 |
} |
| 439 | 439 |
|
| 440 | 440 |
func TestParseEnvfileVariables(t *testing.T) {
|
| 441 |
+ e := "open nonexistent: no such file or directory" |
|
| 442 |
+ if runtime.GOOS == "windows" {
|
|
| 443 |
+ e = "open nonexistent: The system cannot find the file specified." |
|
| 444 |
+ } |
|
| 441 | 445 |
// env ko |
| 442 |
- if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != "open nonexistent: no such file or directory" {
|
|
| 443 |
- t.Fatalf("Expected an error with message 'open nonexistent: no such file or directory', got %v", err)
|
|
| 446 |
+ if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
|
|
| 447 |
+ t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
|
| 444 | 448 |
} |
| 445 | 449 |
// env ok |
| 446 | 450 |
config, _, _, err := parseRun([]string{"--env-file=fixtures/valid.env", "img", "cmd"})
|
| ... | ... |
@@ -460,9 +633,13 @@ func TestParseEnvfileVariables(t *testing.T) {
|
| 460 | 460 |
} |
| 461 | 461 |
|
| 462 | 462 |
func TestParseLabelfileVariables(t *testing.T) {
|
| 463 |
+ e := "open nonexistent: no such file or directory" |
|
| 464 |
+ if runtime.GOOS == "windows" {
|
|
| 465 |
+ e = "open nonexistent: The system cannot find the file specified." |
|
| 466 |
+ } |
|
| 463 | 467 |
// label ko |
| 464 |
- if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != "open nonexistent: no such file or directory" {
|
|
| 465 |
- t.Fatalf("Expected an error with message 'open nonexistent: no such file or directory', got %v", err)
|
|
| 468 |
+ if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
|
|
| 469 |
+ t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
|
| 466 | 470 |
} |
| 467 | 471 |
// label ok |
| 468 | 472 |
config, _, _, err := parseRun([]string{"--label-file=fixtures/valid.label", "img", "cmd"})
|
| ... | ... |
@@ -14,6 +14,8 @@ var ( |
| 14 | 14 |
ErrVolumeInUse = errors.New("volume is in use")
|
| 15 | 15 |
// ErrNoSuchVolume is a typed error returned if the requested volume doesn't exist in the volume store |
| 16 | 16 |
ErrNoSuchVolume = errors.New("no such volume")
|
| 17 |
+ // ErrInvalidName is a typed error returned when creating a volume with a name that is not valid on the platform |
|
| 18 |
+ ErrInvalidName = errors.New("volume name is not valid on this platform")
|
|
| 17 | 19 |
) |
| 18 | 20 |
|
| 19 | 21 |
// New initializes a VolumeStore to keep |
| ... | ... |
@@ -39,13 +41,14 @@ type volumeCounter struct {
|
| 39 | 39 |
// AddAll adds a list of volumes to the store |
| 40 | 40 |
func (s *VolumeStore) AddAll(vols []volume.Volume) {
|
| 41 | 41 |
for _, v := range vols {
|
| 42 |
- s.vols[v.Name()] = &volumeCounter{v, 0}
|
|
| 42 |
+ s.vols[normaliseVolumeName(v.Name())] = &volumeCounter{v, 0}
|
|
| 43 | 43 |
} |
| 44 | 44 |
} |
| 45 | 45 |
|
| 46 | 46 |
// Create tries to find an existing volume with the given name or create a new one from the passed in driver |
| 47 | 47 |
func (s *VolumeStore) Create(name, driverName string, opts map[string]string) (volume.Volume, error) {
|
| 48 | 48 |
s.mu.Lock() |
| 49 |
+ name = normaliseVolumeName(name) |
|
| 49 | 50 |
if vc, exists := s.vols[name]; exists {
|
| 50 | 51 |
v := vc.Volume |
| 51 | 52 |
s.mu.Unlock() |
| ... | ... |
@@ -59,13 +62,22 @@ func (s *VolumeStore) Create(name, driverName string, opts map[string]string) (v |
| 59 | 59 |
return nil, err |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 |
+ // Validate the name in a platform-specific manner |
|
| 63 |
+ valid, err := volume.IsVolumeNameValid(name) |
|
| 64 |
+ if err != nil {
|
|
| 65 |
+ return nil, err |
|
| 66 |
+ } |
|
| 67 |
+ if !valid {
|
|
| 68 |
+ return nil, ErrInvalidName |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 62 | 71 |
v, err := vd.Create(name, opts) |
| 63 | 72 |
if err != nil {
|
| 64 | 73 |
return nil, err |
| 65 | 74 |
} |
| 66 | 75 |
|
| 67 | 76 |
s.mu.Lock() |
| 68 |
- s.vols[v.Name()] = &volumeCounter{v, 0}
|
|
| 77 |
+ s.vols[normaliseVolumeName(v.Name())] = &volumeCounter{v, 0}
|
|
| 69 | 78 |
s.mu.Unlock() |
| 70 | 79 |
|
| 71 | 80 |
return v, nil |
| ... | ... |
@@ -73,6 +85,7 @@ func (s *VolumeStore) Create(name, driverName string, opts map[string]string) (v |
| 73 | 73 |
|
| 74 | 74 |
// Get looks if a volume with the given name exists and returns it if so |
| 75 | 75 |
func (s *VolumeStore) Get(name string) (volume.Volume, error) {
|
| 76 |
+ name = normaliseVolumeName(name) |
|
| 76 | 77 |
s.mu.Lock() |
| 77 | 78 |
defer s.mu.Unlock() |
| 78 | 79 |
vc, exists := s.vols[name] |
| ... | ... |
@@ -86,7 +99,7 @@ func (s *VolumeStore) Get(name string) (volume.Volume, error) {
|
| 86 | 86 |
func (s *VolumeStore) Remove(v volume.Volume) error {
|
| 87 | 87 |
s.mu.Lock() |
| 88 | 88 |
defer s.mu.Unlock() |
| 89 |
- name := v.Name() |
|
| 89 |
+ name := normaliseVolumeName(v.Name()) |
|
| 90 | 90 |
logrus.Debugf("Removing volume reference: driver %s, name %s", v.DriverName(), name)
|
| 91 | 91 |
vc, exists := s.vols[name] |
| 92 | 92 |
if !exists {
|
| ... | ... |
@@ -112,11 +125,12 @@ func (s *VolumeStore) Remove(v volume.Volume) error {
|
| 112 | 112 |
func (s *VolumeStore) Increment(v volume.Volume) {
|
| 113 | 113 |
s.mu.Lock() |
| 114 | 114 |
defer s.mu.Unlock() |
| 115 |
- logrus.Debugf("Incrementing volume reference: driver %s, name %s", v.DriverName(), v.Name())
|
|
| 115 |
+ name := normaliseVolumeName(v.Name()) |
|
| 116 |
+ logrus.Debugf("Incrementing volume reference: driver %s, name %s", v.DriverName(), name)
|
|
| 116 | 117 |
|
| 117 |
- vc, exists := s.vols[v.Name()] |
|
| 118 |
+ vc, exists := s.vols[name] |
|
| 118 | 119 |
if !exists {
|
| 119 |
- s.vols[v.Name()] = &volumeCounter{v, 1}
|
|
| 120 |
+ s.vols[name] = &volumeCounter{v, 1}
|
|
| 120 | 121 |
return |
| 121 | 122 |
} |
| 122 | 123 |
vc.count++ |
| ... | ... |
@@ -126,9 +140,10 @@ func (s *VolumeStore) Increment(v volume.Volume) {
|
| 126 | 126 |
func (s *VolumeStore) Decrement(v volume.Volume) {
|
| 127 | 127 |
s.mu.Lock() |
| 128 | 128 |
defer s.mu.Unlock() |
| 129 |
- logrus.Debugf("Decrementing volume reference: driver %s, name %s", v.DriverName(), v.Name())
|
|
| 129 |
+ name := normaliseVolumeName(v.Name()) |
|
| 130 |
+ logrus.Debugf("Decrementing volume reference: driver %s, name %s", v.DriverName(), name)
|
|
| 130 | 131 |
|
| 131 |
- vc, exists := s.vols[v.Name()] |
|
| 132 |
+ vc, exists := s.vols[name] |
|
| 132 | 133 |
if !exists {
|
| 133 | 134 |
return |
| 134 | 135 |
} |
| ... | ... |
@@ -142,7 +157,7 @@ func (s *VolumeStore) Decrement(v volume.Volume) {
|
| 142 | 142 |
func (s *VolumeStore) Count(v volume.Volume) uint {
|
| 143 | 143 |
s.mu.Lock() |
| 144 | 144 |
defer s.mu.Unlock() |
| 145 |
- vc, exists := s.vols[v.Name()] |
|
| 145 |
+ vc, exists := s.vols[normaliseVolumeName(v.Name())] |
|
| 146 | 146 |
if !exists {
|
| 147 | 147 |
return 0 |
| 148 | 148 |
} |
| 149 | 149 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,9 @@ |
| 0 |
+// +build linux freebsd |
|
| 1 |
+ |
|
| 2 |
+package store |
|
| 3 |
+ |
|
| 4 |
+// normaliseVolumeName is a platform specific function to normalise the name |
|
| 5 |
+// of a volume. This is a no-op on Unix-like platforms |
|
| 6 |
+func normaliseVolumeName(name string) string {
|
|
| 7 |
+ return name |
|
| 8 |
+} |
| 0 | 9 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,12 @@ |
| 0 |
+package store |
|
| 1 |
+ |
|
| 2 |
+import "strings" |
|
| 3 |
+ |
|
| 4 |
+// normaliseVolumeName is a platform specific function to normalise the name |
|
| 5 |
+// of a volume. On Windows, as NTFS is case insensitive, under |
|
| 6 |
+// c:\ProgramData\Docker\Volumes\, the folders John and john would be synonymous. |
|
| 7 |
+// Hence we can't allow the volume "John" and "john" to be created as seperate |
|
| 8 |
+// volumes. |
|
| 9 |
+func normaliseVolumeName(name string) string {
|
|
| 10 |
+ return strings.ToLower(name) |
|
| 11 |
+} |
| ... | ... |
@@ -1,5 +1,15 @@ |
| 1 | 1 |
package volume |
| 2 | 2 |
|
| 3 |
+import ( |
|
| 4 |
+ "os" |
|
| 5 |
+ "runtime" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/Sirupsen/logrus" |
|
| 9 |
+ derr "github.com/docker/docker/errors" |
|
| 10 |
+ "github.com/docker/docker/pkg/system" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 3 | 13 |
// DefaultDriverName is the driver name used for the driver |
| 4 | 14 |
// implemented in the local package. |
| 5 | 15 |
const DefaultDriverName string = "local" |
| ... | ... |
@@ -29,33 +39,134 @@ type Volume interface {
|
| 29 | 29 |
Unmount() error |
| 30 | 30 |
} |
| 31 | 31 |
|
| 32 |
-// read-write modes |
|
| 33 |
-var rwModes = map[string]bool{
|
|
| 34 |
- "rw": true, |
|
| 35 |
- "rw,Z": true, |
|
| 36 |
- "rw,z": true, |
|
| 37 |
- "z,rw": true, |
|
| 38 |
- "Z,rw": true, |
|
| 39 |
- "Z": true, |
|
| 40 |
- "z": true, |
|
| 32 |
+// MountPoint is the intersection point between a volume and a container. It |
|
| 33 |
+// specifies which volume is to be used and where inside a container it should |
|
| 34 |
+// be mounted. |
|
| 35 |
+type MountPoint struct {
|
|
| 36 |
+ Source string // Container host directory |
|
| 37 |
+ Destination string // Inside the container |
|
| 38 |
+ RW bool // True if writable |
|
| 39 |
+ Name string // Name set by user |
|
| 40 |
+ Driver string // Volume driver to use |
|
| 41 |
+ Volume Volume `json:"-"` |
|
| 42 |
+ |
|
| 43 |
+ // Note Mode is not used on Windows |
|
| 44 |
+ Mode string `json:"Relabel"` // Originally field was `Relabel`" |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+// Setup sets up a mount point by either mounting the volume if it is |
|
| 48 |
+// configured, or creating the source directory if supplied. |
|
| 49 |
+func (m *MountPoint) Setup() (string, error) {
|
|
| 50 |
+ if m.Volume != nil {
|
|
| 51 |
+ return m.Volume.Mount() |
|
| 52 |
+ } |
|
| 53 |
+ if len(m.Source) > 0 {
|
|
| 54 |
+ if _, err := os.Stat(m.Source); err != nil {
|
|
| 55 |
+ if !os.IsNotExist(err) {
|
|
| 56 |
+ return "", err |
|
| 57 |
+ } |
|
| 58 |
+ if runtime.GOOS != "windows" { // Windows does not have deprecation issues here
|
|
| 59 |
+ logrus.Warnf("Auto-creating non-existant volume host path %s, this is deprecated and will be removed soon", m.Source)
|
|
| 60 |
+ if err := system.MkdirAll(m.Source, 0755); err != nil {
|
|
| 61 |
+ return "", err |
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+ } |
|
| 65 |
+ return m.Source, nil |
|
| 66 |
+ } |
|
| 67 |
+ return "", derr.ErrorCodeMountSetup |
|
| 41 | 68 |
} |
| 42 | 69 |
|
| 43 |
-// read-only modes |
|
| 44 |
-var roModes = map[string]bool{
|
|
| 45 |
- "ro": true, |
|
| 46 |
- "ro,Z": true, |
|
| 47 |
- "ro,z": true, |
|
| 48 |
- "z,ro": true, |
|
| 49 |
- "Z,ro": true, |
|
| 70 |
+// Path returns the path of a volume in a mount point. |
|
| 71 |
+func (m *MountPoint) Path() string {
|
|
| 72 |
+ if m.Volume != nil {
|
|
| 73 |
+ return m.Volume.Path() |
|
| 74 |
+ } |
|
| 75 |
+ return m.Source |
|
| 50 | 76 |
} |
| 51 | 77 |
|
| 52 | 78 |
// ValidMountMode will make sure the mount mode is valid. |
| 53 | 79 |
// returns if it's a valid mount mode or not. |
| 54 | 80 |
func ValidMountMode(mode string) bool {
|
| 55 |
- return roModes[mode] || rwModes[mode] |
|
| 81 |
+ return roModes[strings.ToLower(mode)] || rwModes[strings.ToLower(mode)] |
|
| 56 | 82 |
} |
| 57 | 83 |
|
| 58 | 84 |
// ReadWrite tells you if a mode string is a valid read-write mode or not. |
| 59 | 85 |
func ReadWrite(mode string) bool {
|
| 60 |
- return rwModes[mode] |
|
| 86 |
+ return rwModes[strings.ToLower(mode)] |
|
| 87 |
+} |
|
| 88 |
+ |
|
| 89 |
+// ParseVolumesFrom ensure that the supplied volumes-from is valid. |
|
| 90 |
+func ParseVolumesFrom(spec string) (string, string, error) {
|
|
| 91 |
+ if len(spec) == 0 {
|
|
| 92 |
+ return "", "", derr.ErrorCodeVolumeFromBlank.WithArgs(spec) |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ specParts := strings.SplitN(spec, ":", 2) |
|
| 96 |
+ id := specParts[0] |
|
| 97 |
+ mode := "rw" |
|
| 98 |
+ |
|
| 99 |
+ if len(specParts) == 2 {
|
|
| 100 |
+ mode = specParts[1] |
|
| 101 |
+ if !ValidMountMode(mode) {
|
|
| 102 |
+ return "", "", derr.ErrorCodeVolumeInvalidMode.WithArgs(mode) |
|
| 103 |
+ } |
|
| 104 |
+ } |
|
| 105 |
+ return id, mode, nil |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+// SplitN splits raw into a maximum of n parts, separated by a separator colon. |
|
| 109 |
+// A separator colon is the last `:` character in the regex `[/:\\]?[a-zA-Z]:` (note `\\` is `\` escaped). |
|
| 110 |
+// This allows to correctly split strings such as `C:\foo:D:\:rw`. |
|
| 111 |
+func SplitN(raw string, n int) []string {
|
|
| 112 |
+ var array []string |
|
| 113 |
+ if len(raw) == 0 || raw[0] == ':' {
|
|
| 114 |
+ // invalid |
|
| 115 |
+ return nil |
|
| 116 |
+ } |
|
| 117 |
+ // numberOfParts counts the number of parts separated by a separator colon |
|
| 118 |
+ numberOfParts := 0 |
|
| 119 |
+ // left represents the left-most cursor in raw, updated at every `:` character considered as a separator. |
|
| 120 |
+ left := 0 |
|
| 121 |
+ // right represents the right-most cursor in raw incremented with the loop. Note this |
|
| 122 |
+ // starts at index 1 as index 0 is already handle above as a special case. |
|
| 123 |
+ for right := 1; right < len(raw); right++ {
|
|
| 124 |
+ // stop parsing if reached maximum number of parts |
|
| 125 |
+ if n >= 0 && numberOfParts >= n {
|
|
| 126 |
+ break |
|
| 127 |
+ } |
|
| 128 |
+ if raw[right] != ':' {
|
|
| 129 |
+ continue |
|
| 130 |
+ } |
|
| 131 |
+ potentialDriveLetter := raw[right-1] |
|
| 132 |
+ if (potentialDriveLetter >= 'A' && potentialDriveLetter <= 'Z') || (potentialDriveLetter >= 'a' && potentialDriveLetter <= 'z') {
|
|
| 133 |
+ if right > 1 {
|
|
| 134 |
+ beforePotentialDriveLetter := raw[right-2] |
|
| 135 |
+ if beforePotentialDriveLetter != ':' && beforePotentialDriveLetter != '/' && beforePotentialDriveLetter != '\\' {
|
|
| 136 |
+ // e.g. `C:` is not preceded by any delimiter, therefore it was not a drive letter but a path ending with `C:`. |
|
| 137 |
+ array = append(array, raw[left:right]) |
|
| 138 |
+ left = right + 1 |
|
| 139 |
+ numberOfParts++ |
|
| 140 |
+ } |
|
| 141 |
+ // else, `C:` is considered as a drive letter and not as a delimiter, so we continue parsing. |
|
| 142 |
+ } |
|
| 143 |
+ // if right == 1, then `C:` is the beginning of the raw string, therefore `:` is again not considered a delimiter and we continue parsing. |
|
| 144 |
+ } else {
|
|
| 145 |
+ // if `:` is not preceded by a potential drive letter, then consider it as a delimiter. |
|
| 146 |
+ array = append(array, raw[left:right]) |
|
| 147 |
+ left = right + 1 |
|
| 148 |
+ numberOfParts++ |
|
| 149 |
+ } |
|
| 150 |
+ } |
|
| 151 |
+ // need to take care of the last part |
|
| 152 |
+ if left < len(raw) {
|
|
| 153 |
+ if n >= 0 && numberOfParts >= n {
|
|
| 154 |
+ // if the maximum number of parts is reached, just append the rest to the last part |
|
| 155 |
+ // left-1 is at the last `:` that needs to be included since not considered a separator. |
|
| 156 |
+ array[n-1] += raw[left-1:] |
|
| 157 |
+ } else {
|
|
| 158 |
+ array = append(array, raw[left:]) |
|
| 159 |
+ } |
|
| 160 |
+ } |
|
| 161 |
+ return array |
|
| 61 | 162 |
} |
| 62 | 163 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,261 @@ |
| 0 |
+package volume |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "runtime" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "testing" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 8 |
+func TestParseMountSpec(t *testing.T) {
|
|
| 9 |
+ var ( |
|
| 10 |
+ valid []string |
|
| 11 |
+ invalid map[string]string |
|
| 12 |
+ ) |
|
| 13 |
+ |
|
| 14 |
+ if runtime.GOOS == "windows" {
|
|
| 15 |
+ valid = []string{
|
|
| 16 |
+ `d:\`, |
|
| 17 |
+ `d:`, |
|
| 18 |
+ `d:\path`, |
|
| 19 |
+ `d:\path with space`, |
|
| 20 |
+ // TODO Windows post TP4 - readonly support `d:\pathandmode:ro`, |
|
| 21 |
+ `c:\:d:\`, |
|
| 22 |
+ `c:\windows\:d:`, |
|
| 23 |
+ `c:\windows:d:\s p a c e`, |
|
| 24 |
+ `c:\windows:d:\s p a c e:RW`, |
|
| 25 |
+ `c:\program files:d:\s p a c e i n h o s t d i r`, |
|
| 26 |
+ `0123456789name:d:`, |
|
| 27 |
+ `MiXeDcAsEnAmE:d:`, |
|
| 28 |
+ `name:D:`, |
|
| 29 |
+ `name:D::rW`, |
|
| 30 |
+ `name:D::RW`, |
|
| 31 |
+ // TODO Windows post TP4 - readonly support `name:D::RO`, |
|
| 32 |
+ `c:/:d:/forward/slashes/are/good/too`, |
|
| 33 |
+ // TODO Windows post TP4 - readonly support `c:/:d:/including with/spaces:ro`, |
|
| 34 |
+ `c:\Windows`, // With capital |
|
| 35 |
+ `c:\Program Files (x86)`, // With capitals and brackets |
|
| 36 |
+ } |
|
| 37 |
+ invalid = map[string]string{
|
|
| 38 |
+ ``: "Invalid volume specification: ", |
|
| 39 |
+ `.`: "Invalid volume specification: ", |
|
| 40 |
+ `..\`: "Invalid volume specification: ", |
|
| 41 |
+ `c:\:..\`: "Invalid volume specification: ", |
|
| 42 |
+ `c:\:d:\:xyzzy`: "Invalid volume specification: ", |
|
| 43 |
+ `c:`: "cannot be c:", |
|
| 44 |
+ `c:\`: `cannot be c:\`, |
|
| 45 |
+ `c:\notexist:d:`: `The system cannot find the file specified`, |
|
| 46 |
+ `c:\windows\system32\ntdll.dll:d:`: `Source 'c:\windows\system32\ntdll.dll' is not a directory`, |
|
| 47 |
+ `name<:d:`: `Invalid volume specification`, |
|
| 48 |
+ `name>:d:`: `Invalid volume specification`, |
|
| 49 |
+ `name::d:`: `Invalid volume specification`, |
|
| 50 |
+ `name":d:`: `Invalid volume specification`, |
|
| 51 |
+ `name\:d:`: `Invalid volume specification`, |
|
| 52 |
+ `name*:d:`: `Invalid volume specification`, |
|
| 53 |
+ `name|:d:`: `Invalid volume specification`, |
|
| 54 |
+ `name?:d:`: `Invalid volume specification`, |
|
| 55 |
+ `name/:d:`: `Invalid volume specification`, |
|
| 56 |
+ `d:\pathandmode:rw`: `Invalid volume specification`, |
|
| 57 |
+ `con:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 58 |
+ `PRN:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 59 |
+ `aUx:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 60 |
+ `nul:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 61 |
+ `com1:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 62 |
+ `com2:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 63 |
+ `com3:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 64 |
+ `com4:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 65 |
+ `com5:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 66 |
+ `com6:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 67 |
+ `com7:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 68 |
+ `com8:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 69 |
+ `com9:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 70 |
+ `lpt1:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 71 |
+ `lpt2:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 72 |
+ `lpt3:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 73 |
+ `lpt4:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 74 |
+ `lpt5:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 75 |
+ `lpt6:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 76 |
+ `lpt7:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 77 |
+ `lpt8:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 78 |
+ `lpt9:d:`: `cannot be a reserved word for Windows filenames`, |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ } else {
|
|
| 82 |
+ valid = []string{
|
|
| 83 |
+ "/home", |
|
| 84 |
+ "/home:/home", |
|
| 85 |
+ "/home:/something/else", |
|
| 86 |
+ "/with space", |
|
| 87 |
+ "/home:/with space", |
|
| 88 |
+ "relative:/absolute-path", |
|
| 89 |
+ "hostPath:/containerPath:ro", |
|
| 90 |
+ "/hostPath:/containerPath:rw", |
|
| 91 |
+ "/rw:/ro", |
|
| 92 |
+ } |
|
| 93 |
+ invalid = map[string]string{
|
|
| 94 |
+ "": "Invalid volume specification", |
|
| 95 |
+ "./": "Invalid volume destination", |
|
| 96 |
+ "../": "Invalid volume destination", |
|
| 97 |
+ "/:../": "Invalid volume destination", |
|
| 98 |
+ "/:path": "Invalid volume destination", |
|
| 99 |
+ ":": "Invalid volume specification", |
|
| 100 |
+ "/tmp:": "Invalid volume destination", |
|
| 101 |
+ ":test": "Invalid volume specification", |
|
| 102 |
+ ":/test": "Invalid volume specification", |
|
| 103 |
+ "tmp:": "Invalid volume destination", |
|
| 104 |
+ ":test:": "Invalid volume specification", |
|
| 105 |
+ "::": "Invalid volume specification", |
|
| 106 |
+ ":::": "Invalid volume specification", |
|
| 107 |
+ "/tmp:::": "Invalid volume specification", |
|
| 108 |
+ ":/tmp::": "Invalid volume specification", |
|
| 109 |
+ "/path:rw": "Invalid volume specification", |
|
| 110 |
+ "/path:ro": "Invalid volume specification", |
|
| 111 |
+ "/rw:rw": "Invalid volume specification", |
|
| 112 |
+ "path:ro": "Invalid volume specification", |
|
| 113 |
+ "/path:/path:sw": "invalid mode: sw", |
|
| 114 |
+ "/path:/path:rwz": "invalid mode: rwz", |
|
| 115 |
+ } |
|
| 116 |
+ } |
|
| 117 |
+ |
|
| 118 |
+ for _, path := range valid {
|
|
| 119 |
+ if _, err := ParseMountSpec(path, "local"); err != nil {
|
|
| 120 |
+ t.Fatalf("ParseMountSpec(`%q`) should succeed: error %q", path, err)
|
|
| 121 |
+ } |
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ for path, expectedError := range invalid {
|
|
| 125 |
+ if _, err := ParseMountSpec(path, "local"); err == nil {
|
|
| 126 |
+ t.Fatalf("ParseMountSpec(`%q`) should have failed validation. Err %v", path, err)
|
|
| 127 |
+ } else {
|
|
| 128 |
+ if !strings.Contains(err.Error(), expectedError) {
|
|
| 129 |
+ t.Fatalf("ParseMountSpec(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
|
|
| 130 |
+ } |
|
| 131 |
+ } |
|
| 132 |
+ } |
|
| 133 |
+} |
|
| 134 |
+ |
|
| 135 |
+func TestSplitN(t *testing.T) {
|
|
| 136 |
+ for _, x := range []struct {
|
|
| 137 |
+ input string |
|
| 138 |
+ n int |
|
| 139 |
+ expected []string |
|
| 140 |
+ }{
|
|
| 141 |
+ {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}},
|
|
| 142 |
+ {`:C:\foo:d:`, -1, nil},
|
|
| 143 |
+ {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}},
|
|
| 144 |
+ {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}},
|
|
| 145 |
+ {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}},
|
|
| 146 |
+ |
|
| 147 |
+ {`d:\`, -1, []string{`d:\`}},
|
|
| 148 |
+ {`d:`, -1, []string{`d:`}},
|
|
| 149 |
+ {`d:\path`, -1, []string{`d:\path`}},
|
|
| 150 |
+ {`d:\path with space`, -1, []string{`d:\path with space`}},
|
|
| 151 |
+ {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}},
|
|
| 152 |
+ {`c:\:d:\`, -1, []string{`c:\`, `d:\`}},
|
|
| 153 |
+ {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}},
|
|
| 154 |
+ {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}},
|
|
| 155 |
+ {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}},
|
|
| 156 |
+ {`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}},
|
|
| 157 |
+ {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}},
|
|
| 158 |
+ {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}},
|
|
| 159 |
+ {`name:D:`, -1, []string{`name`, `D:`}},
|
|
| 160 |
+ {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}},
|
|
| 161 |
+ {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}},
|
|
| 162 |
+ {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}},
|
|
| 163 |
+ {`c:\Windows`, -1, []string{`c:\Windows`}},
|
|
| 164 |
+ {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}},
|
|
| 165 |
+ |
|
| 166 |
+ {``, -1, nil},
|
|
| 167 |
+ {`.`, -1, []string{`.`}},
|
|
| 168 |
+ {`..\`, -1, []string{`..\`}},
|
|
| 169 |
+ {`c:\:..\`, -1, []string{`c:\`, `..\`}},
|
|
| 170 |
+ {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}},
|
|
| 171 |
+ } {
|
|
| 172 |
+ res := SplitN(x.input, x.n) |
|
| 173 |
+ if len(res) < len(x.expected) {
|
|
| 174 |
+ t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
|
|
| 175 |
+ } |
|
| 176 |
+ for i, e := range res {
|
|
| 177 |
+ if e != x.expected[i] {
|
|
| 178 |
+ t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
|
|
| 179 |
+ } |
|
| 180 |
+ } |
|
| 181 |
+ } |
|
| 182 |
+} |
|
| 183 |
+ |
|
| 184 |
+// testParseMountSpec is a structure used by TestParseMountSpecSplit for |
|
| 185 |
+// specifying test cases for the ParseMountSpec() function. |
|
| 186 |
+type testParseMountSpec struct {
|
|
| 187 |
+ bind string |
|
| 188 |
+ driver string |
|
| 189 |
+ expDest string |
|
| 190 |
+ expSource string |
|
| 191 |
+ expName string |
|
| 192 |
+ expDriver string |
|
| 193 |
+ expRW bool |
|
| 194 |
+ fail bool |
|
| 195 |
+} |
|
| 196 |
+ |
|
| 197 |
+func TestParseMountSpecSplit(t *testing.T) {
|
|
| 198 |
+ var cases []testParseMountSpec |
|
| 199 |
+ if runtime.GOOS == "windows" {
|
|
| 200 |
+ cases = []testParseMountSpec{
|
|
| 201 |
+ {`c:\:d:`, "local", `d:`, `c:\`, ``, "", true, false},
|
|
| 202 |
+ {`c:\:d:\`, "local", `d:\`, `c:\`, ``, "", true, false},
|
|
| 203 |
+ // TODO Windows post TP4 - Add readonly support {`c:\:d:\:ro`, "local", `d:\`, `c:\`, ``, "", false, false},
|
|
| 204 |
+ {`c:\:d:\:rw`, "local", `d:\`, `c:\`, ``, "", true, false},
|
|
| 205 |
+ {`c:\:d:\:foo`, "local", `d:\`, `c:\`, ``, "", false, true},
|
|
| 206 |
+ {`name:d::rw`, "local", `d:`, ``, `name`, "local", true, false},
|
|
| 207 |
+ {`name:d:`, "local", `d:`, ``, `name`, "local", true, false},
|
|
| 208 |
+ // TODO Windows post TP4 - Add readonly support {`name:d::ro`, "local", `d:`, ``, `name`, "local", false, false},
|
|
| 209 |
+ {`name:c:`, "", ``, ``, ``, "", true, true},
|
|
| 210 |
+ {`driver/name:c:`, "", ``, ``, ``, "", true, true},
|
|
| 211 |
+ } |
|
| 212 |
+ } else {
|
|
| 213 |
+ cases = []testParseMountSpec{
|
|
| 214 |
+ {"/tmp:/tmp1", "", "/tmp1", "/tmp", "", "", true, false},
|
|
| 215 |
+ {"/tmp:/tmp2:ro", "", "/tmp2", "/tmp", "", "", false, false},
|
|
| 216 |
+ {"/tmp:/tmp3:rw", "", "/tmp3", "/tmp", "", "", true, false},
|
|
| 217 |
+ {"/tmp:/tmp4:foo", "", "", "", "", "", false, true},
|
|
| 218 |
+ {"name:/named1", "", "/named1", "", "name", "local", true, false},
|
|
| 219 |
+ {"name:/named2", "external", "/named2", "", "name", "external", true, false},
|
|
| 220 |
+ {"name:/named3:ro", "local", "/named3", "", "name", "local", false, false},
|
|
| 221 |
+ {"local/name:/tmp:rw", "", "/tmp", "", "local/name", "local", true, false},
|
|
| 222 |
+ {"/tmp:tmp", "", "", "", "", "", true, true},
|
|
| 223 |
+ } |
|
| 224 |
+ } |
|
| 225 |
+ |
|
| 226 |
+ for _, c := range cases {
|
|
| 227 |
+ m, err := ParseMountSpec(c.bind, c.driver) |
|
| 228 |
+ if c.fail {
|
|
| 229 |
+ if err == nil {
|
|
| 230 |
+ t.Fatalf("Expected error, was nil, for spec %s\n", c.bind)
|
|
| 231 |
+ } |
|
| 232 |
+ continue |
|
| 233 |
+ } |
|
| 234 |
+ |
|
| 235 |
+ if m == nil || err != nil {
|
|
| 236 |
+ t.Fatalf("ParseMountSpec failed for spec %s driver %s error %v\n", c.bind, c.driver, err.Error())
|
|
| 237 |
+ continue |
|
| 238 |
+ } |
|
| 239 |
+ |
|
| 240 |
+ if m.Destination != c.expDest {
|
|
| 241 |
+ t.Fatalf("Expected destination %s, was %s, for spec %s\n", c.expDest, m.Destination, c.bind)
|
|
| 242 |
+ } |
|
| 243 |
+ |
|
| 244 |
+ if m.Source != c.expSource {
|
|
| 245 |
+ t.Fatalf("Expected source %s, was %s, for spec %s\n", c.expSource, m.Source, c.bind)
|
|
| 246 |
+ } |
|
| 247 |
+ |
|
| 248 |
+ if m.Name != c.expName {
|
|
| 249 |
+ t.Fatalf("Expected name %s, was %s for spec %s\n", c.expName, m.Name, c.bind)
|
|
| 250 |
+ } |
|
| 251 |
+ |
|
| 252 |
+ if m.Driver != c.expDriver {
|
|
| 253 |
+ t.Fatalf("Expected driver %s, was %s, for spec %s\n", c.expDriver, m.Driver, c.bind)
|
|
| 254 |
+ } |
|
| 255 |
+ |
|
| 256 |
+ if m.RW != c.expRW {
|
|
| 257 |
+ t.Fatalf("Expected RW %v, was %v for spec %s\n", c.expRW, m.RW, c.bind)
|
|
| 258 |
+ } |
|
| 259 |
+ } |
|
| 260 |
+} |
| 0 | 261 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,132 @@ |
| 0 |
+// +build linux freebsd darwin |
|
| 1 |
+ |
|
| 2 |
+package volume |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "path/filepath" |
|
| 7 |
+ "strings" |
|
| 8 |
+ |
|
| 9 |
+ derr "github.com/docker/docker/errors" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// read-write modes |
|
| 13 |
+var rwModes = map[string]bool{
|
|
| 14 |
+ "rw": true, |
|
| 15 |
+ "rw,Z": true, |
|
| 16 |
+ "rw,z": true, |
|
| 17 |
+ "z,rw": true, |
|
| 18 |
+ "Z,rw": true, |
|
| 19 |
+ "Z": true, |
|
| 20 |
+ "z": true, |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// read-only modes |
|
| 24 |
+var roModes = map[string]bool{
|
|
| 25 |
+ "ro": true, |
|
| 26 |
+ "ro,Z": true, |
|
| 27 |
+ "ro,z": true, |
|
| 28 |
+ "z,ro": true, |
|
| 29 |
+ "Z,ro": true, |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+// BackwardsCompatible decides whether this mount point can be |
|
| 33 |
+// used in old versions of Docker or not. |
|
| 34 |
+// Only bind mounts and local volumes can be used in old versions of Docker. |
|
| 35 |
+func (m *MountPoint) BackwardsCompatible() bool {
|
|
| 36 |
+ return len(m.Source) > 0 || m.Driver == DefaultDriverName |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+// HasResource checks whether the given absolute path for a container is in |
|
| 40 |
+// this mount point. If the relative path starts with `../` then the resource |
|
| 41 |
+// is outside of this mount point, but we can't simply check for this prefix |
|
| 42 |
+// because it misses `..` which is also outside of the mount, so check both. |
|
| 43 |
+func (m *MountPoint) HasResource(absolutePath string) bool {
|
|
| 44 |
+ relPath, err := filepath.Rel(m.Destination, absolutePath) |
|
| 45 |
+ return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
|
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+// ParseMountSpec validates the configuration of mount information is valid. |
|
| 49 |
+func ParseMountSpec(spec, volumeDriver string) (*MountPoint, error) {
|
|
| 50 |
+ spec = filepath.ToSlash(spec) |
|
| 51 |
+ |
|
| 52 |
+ mp := &MountPoint{
|
|
| 53 |
+ RW: true, |
|
| 54 |
+ } |
|
| 55 |
+ if strings.Count(spec, ":") > 2 {
|
|
| 56 |
+ return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ arr := strings.SplitN(spec, ":", 3) |
|
| 60 |
+ if arr[0] == "" {
|
|
| 61 |
+ return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) |
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ switch len(arr) {
|
|
| 65 |
+ case 1: |
|
| 66 |
+ // Just a destination path in the container |
|
| 67 |
+ mp.Destination = filepath.Clean(arr[0]) |
|
| 68 |
+ case 2: |
|
| 69 |
+ if isValid := ValidMountMode(arr[1]); isValid {
|
|
| 70 |
+ // Destination + Mode is not a valid volume - volumes |
|
| 71 |
+ // cannot include a mode. eg /foo:rw |
|
| 72 |
+ return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) |
|
| 73 |
+ } |
|
| 74 |
+ // Host Source Path or Name + Destination |
|
| 75 |
+ mp.Source = arr[0] |
|
| 76 |
+ mp.Destination = arr[1] |
|
| 77 |
+ case 3: |
|
| 78 |
+ // HostSourcePath+DestinationPath+Mode |
|
| 79 |
+ mp.Source = arr[0] |
|
| 80 |
+ mp.Destination = arr[1] |
|
| 81 |
+ mp.Mode = arr[2] // Mode field is used by SELinux to decide whether to apply label |
|
| 82 |
+ if !ValidMountMode(mp.Mode) {
|
|
| 83 |
+ return nil, derr.ErrorCodeVolumeInvalidMode.WithArgs(mp.Mode) |
|
| 84 |
+ } |
|
| 85 |
+ mp.RW = ReadWrite(mp.Mode) |
|
| 86 |
+ default: |
|
| 87 |
+ return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ //validate the volumes destination path |
|
| 91 |
+ mp.Destination = filepath.Clean(mp.Destination) |
|
| 92 |
+ if !filepath.IsAbs(mp.Destination) {
|
|
| 93 |
+ return nil, derr.ErrorCodeVolumeAbs.WithArgs(mp.Destination) |
|
| 94 |
+ } |
|
| 95 |
+ |
|
| 96 |
+ // Destination cannot be "/" |
|
| 97 |
+ if mp.Destination == "/" {
|
|
| 98 |
+ return nil, derr.ErrorCodeVolumeSlash.WithArgs(spec) |
|
| 99 |
+ } |
|
| 100 |
+ |
|
| 101 |
+ name, source := ParseVolumeSource(mp.Source) |
|
| 102 |
+ if len(source) == 0 {
|
|
| 103 |
+ mp.Source = "" // Clear it out as we previously assumed it was not a name |
|
| 104 |
+ mp.Driver = volumeDriver |
|
| 105 |
+ if len(mp.Driver) == 0 {
|
|
| 106 |
+ mp.Driver = DefaultDriverName |
|
| 107 |
+ } |
|
| 108 |
+ } else {
|
|
| 109 |
+ mp.Source = filepath.Clean(source) |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ mp.Name = name |
|
| 113 |
+ |
|
| 114 |
+ return mp, nil |
|
| 115 |
+} |
|
| 116 |
+ |
|
| 117 |
+// ParseVolumeSource parses the origin sources that's mounted into the container. |
|
| 118 |
+// It returns a name and a source. It looks to see if the spec passed in |
|
| 119 |
+// is an absolute file. If it is, it assumes the spec is a source. If not, |
|
| 120 |
+// it assumes the spec is a name. |
|
| 121 |
+func ParseVolumeSource(spec string) (string, string) {
|
|
| 122 |
+ if !filepath.IsAbs(spec) {
|
|
| 123 |
+ return spec, "" |
|
| 124 |
+ } |
|
| 125 |
+ return "", spec |
|
| 126 |
+} |
|
| 127 |
+ |
|
| 128 |
+// IsVolumeNameValid checks a volume name in a platform specific manner. |
|
| 129 |
+func IsVolumeNameValid(name string) (bool, error) {
|
|
| 130 |
+ return true, nil |
|
| 131 |
+} |
| 0 | 132 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,181 @@ |
| 0 |
+package volume |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "os" |
|
| 4 |
+ "path/filepath" |
|
| 5 |
+ "regexp" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/Sirupsen/logrus" |
|
| 9 |
+ derr "github.com/docker/docker/errors" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// read-write modes |
|
| 13 |
+var rwModes = map[string]bool{
|
|
| 14 |
+ "rw": true, |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+// read-only modes |
|
| 18 |
+var roModes = map[string]bool{
|
|
| 19 |
+ "ro": true, |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+const ( |
|
| 23 |
+ // Spec should be in the format [source:]destination[:mode] |
|
| 24 |
+ // |
|
| 25 |
+ // Examples: c:\foo bar:d:rw |
|
| 26 |
+ // c:\foo:d:\bar |
|
| 27 |
+ // myname:d: |
|
| 28 |
+ // d:\ |
|
| 29 |
+ // |
|
| 30 |
+ // Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See |
|
| 31 |
+ // https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to |
|
| 32 |
+ // test is https://regex-golang.appspot.com/assets/html/index.html |
|
| 33 |
+ // |
|
| 34 |
+ // Useful link for referencing named capturing groups: |
|
| 35 |
+ // http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex |
|
| 36 |
+ // |
|
| 37 |
+ // There are three match groups: source, destination and mode. |
|
| 38 |
+ // |
|
| 39 |
+ |
|
| 40 |
+ // RXHostDir is the first option of a source |
|
| 41 |
+ RXHostDir = `[a-z]:\\(?:[^\\/:*?"<>|\r\n]+\\?)*` |
|
| 42 |
+ // RXName is the second option of a source |
|
| 43 |
+ RXName = `[^\\/:*?"<>|\r\n]+` |
|
| 44 |
+ // RXReservedNames are reserved names not possible on Windows |
|
| 45 |
+ RXReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])` |
|
| 46 |
+ |
|
| 47 |
+ // RXSource is the combined possiblities for a source |
|
| 48 |
+ RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `))):)?` |
|
| 49 |
+ |
|
| 50 |
+ // Source. Can be either a host directory, a name, or omitted: |
|
| 51 |
+ // HostDir: |
|
| 52 |
+ // - Essentially using the folder solution from |
|
| 53 |
+ // https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html |
|
| 54 |
+ // but adding case insensitivity. |
|
| 55 |
+ // - Must be an absolute path such as c:\path |
|
| 56 |
+ // - Can include spaces such as `c:\program files` |
|
| 57 |
+ // - And then followed by a colon which is not in the capture group |
|
| 58 |
+ // - And can be optional |
|
| 59 |
+ // Name: |
|
| 60 |
+ // - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx) |
|
| 61 |
+ // - And then followed by a colon which is not in the capture group |
|
| 62 |
+ // - And can be optional |
|
| 63 |
+ |
|
| 64 |
+ // RXDestination is the regex expression for the mount destination |
|
| 65 |
+ RXDestination = `(?P<destination>([a-z]):((?:\\[^\\/:*?"<>\r\n]+)*\\?))` |
|
| 66 |
+ // Destination (aka container path): |
|
| 67 |
+ // - Variation on hostdir but can be a drive followed by colon as well |
|
| 68 |
+ // - If a path, must be absolute. Can include spaces |
|
| 69 |
+ // - Drive cannot be c: (explicitly checked in code, not RegEx) |
|
| 70 |
+ // |
|
| 71 |
+ |
|
| 72 |
+ // RXMode is the regex expression for the mode of the mount |
|
| 73 |
+ RXMode = `(:(?P<mode>(?i)rw))?` |
|
| 74 |
+ // Temporarily for TP4, disabling the use of ro as it's not supported yet |
|
| 75 |
+ // in the platform. TODO Windows: `(:(?P<mode>(?i)ro|rw))?` |
|
| 76 |
+ // mode (optional) |
|
| 77 |
+ // - Hopefully self explanatory in comparison to above. |
|
| 78 |
+ // - Colon is not in the capture group |
|
| 79 |
+ // |
|
| 80 |
+) |
|
| 81 |
+ |
|
| 82 |
+// ParseMountSpec validates the configuration of mount information is valid. |
|
| 83 |
+func ParseMountSpec(spec string, volumeDriver string) (*MountPoint, error) {
|
|
| 84 |
+ var specExp = regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`) |
|
| 85 |
+ |
|
| 86 |
+ // Ensure in platform semantics for matching. The CLI will send in Unix semantics. |
|
| 87 |
+ match := specExp.FindStringSubmatch(filepath.FromSlash(strings.ToLower(spec))) |
|
| 88 |
+ |
|
| 89 |
+ // Must have something back |
|
| 90 |
+ if len(match) == 0 {
|
|
| 91 |
+ return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ // Pull out the sub expressions from the named capture groups |
|
| 95 |
+ matchgroups := make(map[string]string) |
|
| 96 |
+ for i, name := range specExp.SubexpNames() {
|
|
| 97 |
+ matchgroups[name] = strings.ToLower(match[i]) |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ mp := &MountPoint{
|
|
| 101 |
+ Source: matchgroups["source"], |
|
| 102 |
+ Destination: matchgroups["destination"], |
|
| 103 |
+ RW: true, |
|
| 104 |
+ } |
|
| 105 |
+ if strings.ToLower(matchgroups["mode"]) == "ro" {
|
|
| 106 |
+ mp.RW = false |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ // Volumes cannot include an explicitly supplied mode eg c:\path:rw |
|
| 110 |
+ if mp.Source == "" && mp.Destination != "" && matchgroups["mode"] != "" {
|
|
| 111 |
+ return nil, derr.ErrorCodeVolumeInvalid.WithArgs(spec) |
|
| 112 |
+ } |
|
| 113 |
+ |
|
| 114 |
+ // Note: No need to check if destination is absolute as it must be by |
|
| 115 |
+ // definition of matching the regex. |
|
| 116 |
+ |
|
| 117 |
+ if filepath.VolumeName(mp.Destination) == mp.Destination {
|
|
| 118 |
+ // Ensure the destination path, if a drive letter, is not the c drive |
|
| 119 |
+ if strings.ToLower(mp.Destination) == "c:" {
|
|
| 120 |
+ return nil, derr.ErrorCodeVolumeDestIsC.WithArgs(spec) |
|
| 121 |
+ } |
|
| 122 |
+ } else {
|
|
| 123 |
+ // So we know the destination is a path, not drive letter. Clean it up. |
|
| 124 |
+ mp.Destination = filepath.Clean(mp.Destination) |
|
| 125 |
+ // Ensure the destination path, if a path, is not the c root directory |
|
| 126 |
+ if strings.ToLower(mp.Destination) == `c:\` {
|
|
| 127 |
+ return nil, derr.ErrorCodeVolumeDestIsCRoot.WithArgs(spec) |
|
| 128 |
+ } |
|
| 129 |
+ } |
|
| 130 |
+ |
|
| 131 |
+ // See if the source is a name instead of a host directory |
|
| 132 |
+ if len(mp.Source) > 0 {
|
|
| 133 |
+ validName, err := IsVolumeNameValid(mp.Source) |
|
| 134 |
+ if err != nil {
|
|
| 135 |
+ return nil, err |
|
| 136 |
+ } |
|
| 137 |
+ if validName {
|
|
| 138 |
+ // OK, so the source is a name. |
|
| 139 |
+ mp.Name = mp.Source |
|
| 140 |
+ mp.Source = "" |
|
| 141 |
+ |
|
| 142 |
+ // Set the driver accordingly |
|
| 143 |
+ mp.Driver = volumeDriver |
|
| 144 |
+ if len(mp.Driver) == 0 {
|
|
| 145 |
+ mp.Driver = DefaultDriverName |
|
| 146 |
+ } |
|
| 147 |
+ } else {
|
|
| 148 |
+ // OK, so the source must be a host directory. Make sure it's clean. |
|
| 149 |
+ mp.Source = filepath.Clean(mp.Source) |
|
| 150 |
+ } |
|
| 151 |
+ } |
|
| 152 |
+ |
|
| 153 |
+ // Ensure the host path source, if supplied, exists and is a directory |
|
| 154 |
+ if len(mp.Source) > 0 {
|
|
| 155 |
+ var fi os.FileInfo |
|
| 156 |
+ var err error |
|
| 157 |
+ if fi, err = os.Stat(mp.Source); err != nil {
|
|
| 158 |
+ return nil, derr.ErrorCodeVolumeSourceNotFound.WithArgs(mp.Source, err) |
|
| 159 |
+ } |
|
| 160 |
+ if !fi.IsDir() {
|
|
| 161 |
+ return nil, derr.ErrorCodeVolumeSourceNotDirectory.WithArgs(mp.Source) |
|
| 162 |
+ } |
|
| 163 |
+ } |
|
| 164 |
+ |
|
| 165 |
+ logrus.Debugf("MP: Source '%s', Dest '%s', RW %t, Name '%s', Driver '%s'", mp.Source, mp.Destination, mp.RW, mp.Name, mp.Driver)
|
|
| 166 |
+ return mp, nil |
|
| 167 |
+} |
|
| 168 |
+ |
|
| 169 |
+// IsVolumeNameValid checks a volume name in a platform specific manner. |
|
| 170 |
+func IsVolumeNameValid(name string) (bool, error) {
|
|
| 171 |
+ nameExp := regexp.MustCompile(`^` + RXName + `$`) |
|
| 172 |
+ if !nameExp.MatchString(name) {
|
|
| 173 |
+ return false, nil |
|
| 174 |
+ } |
|
| 175 |
+ nameExp = regexp.MustCompile(`^` + RXReservedNames + `$`) |
|
| 176 |
+ if nameExp.MatchString(name) {
|
|
| 177 |
+ return false, derr.ErrorCodeVolumeNameReservedWord.WithArgs(name) |
|
| 178 |
+ } |
|
| 179 |
+ return true, nil |
|
| 180 |
+} |