Browse code

Show volume options for `docker volume inspect`

This fix tries to address the issue raised in 25545 where
volume options at the creation time is not showed up
in `docker volume inspect`.

This fix adds the field `Options` in `Volume` type and
persist the options in volume db so that `volume inspect`
could display the options.

This fix adds a couple of test cases to cover the changes.

This fix fixes 25545.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2016/09/18 04:32:31
Showing 8 changed files
... ...
@@ -447,6 +447,7 @@ type Volume struct {
447 447
 	Status     map[string]interface{} `json:",omitempty"` // Status provides low-level status information about the volume
448 448
 	Labels     map[string]string      // Labels is metadata specific to the volume
449 449
 	Scope      string                 // Scope describes the level at which the volume exists (e.g. `global` for cluster-wide or `local` for machine level)
450
+	Options    map[string]string      // Options holds the driver specific options to use for when creating the volume.
450 451
 	UsageData  *VolumeUsageData       `json:",omitempty"`
451 452
 }
452 453
 
... ...
@@ -608,7 +608,7 @@ func (daemon *Daemon) filterVolumes(vols []volume.Volume, filter filters.Args) (
608 608
 			}
609 609
 		}
610 610
 		if filter.Include("label") {
611
-			v, ok := vol.(volume.LabeledVolume)
611
+			v, ok := vol.(volume.DetailedVolume)
612 612
 			if !ok {
613 613
 				continue
614 614
 			}
... ...
@@ -32,13 +32,12 @@ func volumeToAPIType(v volume.Volume) *types.Volume {
32 32
 		Name:   v.Name(),
33 33
 		Driver: v.DriverName(),
34 34
 	}
35
-	if v, ok := v.(volume.LabeledVolume); ok {
35
+	if v, ok := v.(volume.DetailedVolume); ok {
36 36
 		tv.Labels = v.Labels()
37
-	}
38
-
39
-	if v, ok := v.(volume.ScopedVolume); ok {
37
+		tv.Options = v.Options()
40 38
 		tv.Scope = v.Scope()
41 39
 	}
40
+
42 41
 	return tv
43 42
 }
44 43
 
... ...
@@ -132,6 +132,7 @@ This section lists each version from latest to oldest.  Each listing includes a
132 132
 * `POST /services/create` and `POST /services/(id or name)/update` now accept `Monitor` and `MaxFailureRatio` parameters, which control the response to failures during service updates.
133 133
 * `GET /networks/(name)` now returns `Created`.
134 134
 * `POST /containers/(id or name)/exec` now accepts an `Env` field, which holds a list of environment variables to be set in the context of the command execution.
135
+* `GET /volumes`, `GET /volumes/(name)`, and `POST /volumes/create` now return the `Options` field which holds the driver specific options to use for when creating the volume.
135 136
 
136 137
 ### v1.24 API changes
137 138
 
... ...
@@ -2661,6 +2661,7 @@ Return docker data usage information
2661 2661
                     "Mountpoint": "",
2662 2662
                     "Labels": null,
2663 2663
                     "Scope": "",
2664
+                    "Options": null
2664 2665
                     "UsageData": {
2665 2666
                         "Size": 0,
2666 2667
                         "RefCount": 0
... ...
@@ -3328,7 +3329,12 @@ Return low-level information about the `exec` command `id`.
3328 3328
             "com.example.some-label": "some-value",
3329 3329
             "com.example.some-other-label": "some-other-value"
3330 3330
           },
3331
-          "Scope": "local"
3331
+          "Scope": "local",
3332
+          "Options": {
3333
+            "device": "tmpfs",
3334
+            "o": "size=100m,uid=1000",
3335
+            "type": "tmpfs"
3336
+          }
3332 3337
         }
3333 3338
       ],
3334 3339
       "Warnings": []
... ...
@@ -3382,7 +3388,8 @@ Create a volume
3382 3382
         "com.example.some-label": "some-value",
3383 3383
         "com.example.some-other-label": "some-other-value"
3384 3384
       },
3385
-      "Scope": "local"
3385
+      "Scope": "local",
3386
+      "Options": null
3386 3387
     }
3387 3388
 
3388 3389
 **Status codes**:
... ...
@@ -3429,7 +3436,11 @@ Return low-level information on the volume `name`
3429 3429
           "com.example.some-label": "some-value",
3430 3430
           "com.example.some-other-label": "some-other-value"
3431 3431
       },
3432
-      "Scope": "local"
3432
+      "Scope": "local",
3433
+      "Options": {
3434
+          "some-key": "some-value",
3435
+          "some-other-key": "some-other-value"
3436
+      },
3433 3437
     }
3434 3438
 
3435 3439
 **Status codes**:
... ...
@@ -3454,6 +3465,7 @@ response.
3454 3454
 - **Labels** - Labels set on the volume, specified as a map: `{"key":"value","key2":"value2"}`.
3455 3455
 - **Scope** - Scope describes the level at which the volume exists, can be one of
3456 3456
     `global` for cluster-wide or `local` for machine level. The default is `local`.
3457
+- **Options** - Options holds the driver specific options to use for when creating the volume.
3457 3458
 
3458 3459
 ### Remove a volume
3459 3460
 
... ...
@@ -1,6 +1,7 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"io/ioutil"
5 6
 	"os"
6 7
 	"os/exec"
... ...
@@ -418,3 +419,24 @@ func (s *DockerSuite) TestVolumeCLIRmForce(c *check.C) {
418 418
 	out, _ = dockerCmd(c, "volume", "ls")
419 419
 	c.Assert(out, checker.Contains, name)
420 420
 }
421
+
422
+func (s *DockerSuite) TestVolumeCliInspectWithVolumeOpts(c *check.C) {
423
+	testRequires(c, DaemonIsLinux)
424
+
425
+	// Without options
426
+	name := "test1"
427
+	dockerCmd(c, "volume", "create", "-d", "local", name)
428
+	out, _ := dockerCmd(c, "volume", "inspect", "--format={{ .Options }}", name)
429
+	c.Assert(strings.TrimSpace(out), checker.Contains, "map[]")
430
+
431
+	// With options
432
+	name = "test2"
433
+	k1, v1 := "type", "tmpfs"
434
+	k2, v2 := "device", "tmpfs"
435
+	k3, v3 := "o", "size=1m,uid=1000"
436
+	dockerCmd(c, "volume", "create", "-d", "local", name, "--opt", fmt.Sprintf("%s=%s", k1, v1), "--opt", fmt.Sprintf("%s=%s", k2, v2), "--opt", fmt.Sprintf("%s=%s", k3, v3))
437
+	out, _ = dockerCmd(c, "volume", "inspect", "--format={{ .Options }}", name)
438
+	c.Assert(strings.TrimSpace(out), checker.Contains, fmt.Sprintf("%s:%s", k1, v1))
439
+	c.Assert(strings.TrimSpace(out), checker.Contains, fmt.Sprintf("%s:%s", k2, v2))
440
+	c.Assert(strings.TrimSpace(out), checker.Contains, fmt.Sprintf("%s:%s", k3, v3))
441
+}
... ...
@@ -23,14 +23,24 @@ const (
23 23
 )
24 24
 
25 25
 type volumeMetadata struct {
26
-	Name   string
27
-	Labels map[string]string
26
+	Name    string
27
+	Labels  map[string]string
28
+	Options map[string]string
28 29
 }
29 30
 
30 31
 type volumeWrapper struct {
31 32
 	volume.Volume
32
-	labels map[string]string
33
-	scope  string
33
+	labels  map[string]string
34
+	scope   string
35
+	options map[string]string
36
+}
37
+
38
+func (v volumeWrapper) Options() map[string]string {
39
+	options := map[string]string{}
40
+	for key, value := range v.options {
41
+		options[key] = value
42
+	}
43
+	return options
34 44
 }
35 45
 
36 46
 func (v volumeWrapper) Labels() map[string]string {
... ...
@@ -54,10 +64,11 @@ func (v volumeWrapper) CachedPath() string {
54 54
 // reference counting of volumes in the system.
55 55
 func New(rootPath string) (*VolumeStore, error) {
56 56
 	vs := &VolumeStore{
57
-		locks:  &locker.Locker{},
58
-		names:  make(map[string]volume.Volume),
59
-		refs:   make(map[string][]string),
60
-		labels: make(map[string]map[string]string),
57
+		locks:   &locker.Locker{},
58
+		names:   make(map[string]volume.Volume),
59
+		refs:    make(map[string][]string),
60
+		labels:  make(map[string]map[string]string),
61
+		options: make(map[string]map[string]string),
61 62
 	}
62 63
 
63 64
 	if rootPath != "" {
... ...
@@ -113,6 +124,7 @@ func (s *VolumeStore) Purge(name string) {
113 113
 	delete(s.names, name)
114 114
 	delete(s.refs, name)
115 115
 	delete(s.labels, name)
116
+	delete(s.options, name)
116 117
 	s.globalLock.Unlock()
117 118
 }
118 119
 
... ...
@@ -127,7 +139,9 @@ type VolumeStore struct {
127 127
 	refs map[string][]string
128 128
 	// labels stores volume labels for each volume
129 129
 	labels map[string]map[string]string
130
-	db     *bolt.DB
130
+	// options stores volume options for each volume
131
+	options map[string]map[string]string
132
+	db      *bolt.DB
131 133
 }
132 134
 
133 135
 // List proxies to all registered volume drivers to get the full list of volumes
... ...
@@ -186,7 +200,7 @@ func (s *VolumeStore) list() ([]volume.Volume, []string, error) {
186 186
 				return
187 187
 			}
188 188
 			for i, v := range vs {
189
-				vs[i] = volumeWrapper{v, s.labels[v.Name()], d.Scope()}
189
+				vs[i] = volumeWrapper{v, s.labels[v.Name()], d.Scope(), s.options[v.Name()]}
190 190
 			}
191 191
 
192 192
 			chVols <- vols{vols: vs}
... ...
@@ -283,12 +297,14 @@ func (s *VolumeStore) create(name, driverName string, opts, labels map[string]st
283 283
 	}
284 284
 	s.globalLock.Lock()
285 285
 	s.labels[name] = labels
286
+	s.options[name] = opts
286 287
 	s.globalLock.Unlock()
287 288
 
288 289
 	if s.db != nil {
289 290
 		metadata := &volumeMetadata{
290
-			Name:   name,
291
-			Labels: labels,
291
+			Name:    name,
292
+			Labels:  labels,
293
+			Options: opts,
292 294
 		}
293 295
 
294 296
 		volData, err := json.Marshal(metadata)
... ...
@@ -305,7 +321,7 @@ func (s *VolumeStore) create(name, driverName string, opts, labels map[string]st
305 305
 		}
306 306
 	}
307 307
 
308
-	return volumeWrapper{v, labels, vd.Scope()}, nil
308
+	return volumeWrapper{v, labels, vd.Scope(), opts}, nil
309 309
 }
310 310
 
311 311
 // GetWithRef gets a volume with the given name from the passed in driver and stores the ref
... ...
@@ -328,7 +344,7 @@ func (s *VolumeStore) GetWithRef(name, driverName, ref string) (volume.Volume, e
328 328
 
329 329
 	s.setNamed(v, ref)
330 330
 
331
-	return volumeWrapper{v, s.labels[name], vd.Scope()}, nil
331
+	return volumeWrapper{v, s.labels[name], vd.Scope(), s.options[name]}, nil
332 332
 }
333 333
 
334 334
 // Get looks if a volume with the given name exists and returns it if so
... ...
@@ -350,6 +366,7 @@ func (s *VolumeStore) Get(name string) (volume.Volume, error) {
350 350
 // it is expected that callers of this function hold any necessary locks
351 351
 func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
352 352
 	labels := map[string]string{}
353
+	options := map[string]string{}
353 354
 
354 355
 	if s.db != nil {
355 356
 		// get meta
... ...
@@ -368,6 +385,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
368 368
 				return err
369 369
 			}
370 370
 			labels = meta.Labels
371
+			options = meta.Options
371 372
 
372 373
 			return nil
373 374
 		}); err != nil {
... ...
@@ -388,7 +406,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
388 388
 		if err != nil {
389 389
 			return nil, err
390 390
 		}
391
-		return volumeWrapper{vol, labels, vd.Scope()}, nil
391
+		return volumeWrapper{vol, labels, vd.Scope(), options}, nil
392 392
 	}
393 393
 
394 394
 	logrus.Debugf("Probing all drivers for volume with name: %s", name)
... ...
@@ -403,7 +421,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) {
403 403
 			continue
404 404
 		}
405 405
 
406
-		return volumeWrapper{v, labels, d.Scope()}, nil
406
+		return volumeWrapper{v, labels, d.Scope(), options}, nil
407 407
 	}
408 408
 	return nil, errNoSuchVolume
409 409
 }
... ...
@@ -478,7 +496,11 @@ func (s *VolumeStore) FilterByDriver(name string) ([]volume.Volume, error) {
478 478
 		return nil, &OpErr{Err: err, Name: name, Op: "list"}
479 479
 	}
480 480
 	for i, v := range ls {
481
-		ls[i] = volumeWrapper{v, s.labels[v.Name()], vd.Scope()}
481
+		options := map[string]string{}
482
+		for key, value := range s.options[v.Name()] {
483
+			options[key] = value
484
+		}
485
+		ls[i] = volumeWrapper{v, s.labels[v.Name()], vd.Scope(), options}
482 486
 	}
483 487
 	return ls, nil
484 488
 }
... ...
@@ -68,14 +68,10 @@ type Volume interface {
68 68
 	Status() map[string]interface{}
69 69
 }
70 70
 
71
-// LabeledVolume wraps a Volume with user-defined labels
72
-type LabeledVolume interface {
71
+// DetailedVolume wraps a Volume with user-defined labels, options, and cluster scope (e.g., `local` or `global`)
72
+type DetailedVolume interface {
73 73
 	Labels() map[string]string
74
-	Volume
75
-}
76
-
77
-// ScopedVolume wraps a volume with a cluster scope (e.g., `local` or `global`)
78
-type ScopedVolume interface {
74
+	Options() map[string]string
79 75
 	Scope() string
80 76
 	Volume
81 77
 }