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>
| ... | ... |
@@ -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 |
|
| ... | ... |
@@ -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 |
} |