This is similar to network scopes where a volume can either be `local`
or `global`. A `global` volume is one that exists across the entire
cluster where as a `local` volume exists on a single engine.
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
... | ... |
@@ -745,7 +745,9 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore, |
745 | 745 |
return nil, err |
746 | 746 |
} |
747 | 747 |
|
748 |
- volumedrivers.Register(volumesDriver, volumesDriver.Name()) |
|
748 |
+ if !volumedrivers.Register(volumesDriver, volumesDriver.Name()) { |
|
749 |
+ return nil, fmt.Errorf("local volume driver could not be registered") |
|
750 |
+ } |
|
749 | 751 |
return store.New(config.Root) |
750 | 752 |
} |
751 | 753 |
|
... | ... |
@@ -27,11 +27,13 @@ func volumeToAPIType(v volume.Volume) *types.Volume { |
27 | 27 |
Name: v.Name(), |
28 | 28 |
Driver: v.DriverName(), |
29 | 29 |
} |
30 |
- if v, ok := v.(interface { |
|
31 |
- Labels() map[string]string |
|
32 |
- }); ok { |
|
30 |
+ if v, ok := v.(volume.LabeledVolume); ok { |
|
33 | 31 |
tv.Labels = v.Labels() |
34 | 32 |
} |
33 |
+ |
|
34 |
+ if v, ok := v.(volume.ScopedVolume); ok { |
|
35 |
+ tv.Scope = v.Scope() |
|
36 |
+ } |
|
35 | 37 |
return tv |
36 | 38 |
} |
37 | 39 |
|
... | ... |
@@ -20,6 +20,7 @@ documentation](plugins.md) for more information. |
20 | 20 |
### 1.12.0 |
21 | 21 |
|
22 | 22 |
- Add `Status` field to `VolumeDriver.Get` response ([#21006](https://github.com/docker/docker/pull/21006#)) |
23 |
+- Add `VolumeDriver.Capabilities` to get capabilities of the volume driver([#22077](https://github.com/docker/docker/pull/22077)) |
|
23 | 24 |
|
24 | 25 |
### 1.10.0 |
25 | 26 |
|
... | ... |
@@ -236,3 +237,29 @@ Get the list of volumes registered with the plugin. |
236 | 236 |
``` |
237 | 237 |
|
238 | 238 |
Respond with a string error if an error occurred. |
239 |
+ |
|
240 |
+### /VolumeDriver.Capabilities |
|
241 |
+ |
|
242 |
+**Request**: |
|
243 |
+```json |
|
244 |
+{} |
|
245 |
+``` |
|
246 |
+ |
|
247 |
+Get the list of capabilities the driver supports. |
|
248 |
+The driver is not required to implement this endpoint, however in such cases |
|
249 |
+the default values will be taken. |
|
250 |
+ |
|
251 |
+**Response**: |
|
252 |
+```json |
|
253 |
+{ |
|
254 |
+ "Capabilities": { |
|
255 |
+ "Scope": "global" |
|
256 |
+ } |
|
257 |
+} |
|
258 |
+``` |
|
259 |
+ |
|
260 |
+Supported scopes are `global` and `local`. Any other value in `Scope` will be |
|
261 |
+ignored and assumed to be `local`. Scope allows cluster managers to handle the |
|
262 |
+volume differently, for instance with a scope of `global`, the cluster manager |
|
263 |
+knows it only needs to create the volume once instead of on every engine. More |
|
264 |
+capabilities may be added in the future. |
... | ... |
@@ -16,6 +16,7 @@ import ( |
16 | 16 |
"time" |
17 | 17 |
|
18 | 18 |
"github.com/docker/docker/pkg/integration/checker" |
19 |
+ "github.com/docker/docker/volume" |
|
19 | 20 |
"github.com/docker/engine-api/types" |
20 | 21 |
"github.com/go-check/check" |
21 | 22 |
) |
... | ... |
@@ -35,6 +36,7 @@ type eventCounter struct { |
35 | 35 |
paths int |
36 | 36 |
lists int |
37 | 37 |
gets int |
38 |
+ caps int |
|
38 | 39 |
} |
39 | 40 |
|
40 | 41 |
type DockerExternalVolumeSuite struct { |
... | ... |
@@ -225,6 +227,18 @@ func (s *DockerExternalVolumeSuite) SetUpSuite(c *check.C) { |
225 | 225 |
send(w, nil) |
226 | 226 |
}) |
227 | 227 |
|
228 |
+ mux.HandleFunc("/VolumeDriver.Capabilities", func(w http.ResponseWriter, r *http.Request) { |
|
229 |
+ s.ec.caps++ |
|
230 |
+ |
|
231 |
+ _, err := read(r.Body) |
|
232 |
+ if err != nil { |
|
233 |
+ send(w, err) |
|
234 |
+ return |
|
235 |
+ } |
|
236 |
+ |
|
237 |
+ send(w, `{"Capabilities": { "Scope": "global" }}`) |
|
238 |
+ }) |
|
239 |
+ |
|
228 | 240 |
err := os.MkdirAll("/etc/docker/plugins", 0755) |
229 | 241 |
c.Assert(err, checker.IsNil) |
230 | 242 |
|
... | ... |
@@ -491,3 +505,18 @@ func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverMountID(c *check.C) |
491 | 491 |
c.Assert(err, checker.IsNil, check.Commentf(out)) |
492 | 492 |
c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") |
493 | 493 |
} |
494 |
+ |
|
495 |
+// Check that VolumeDriver.Capabilities gets called, and only called once |
|
496 |
+func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverCapabilities(c *check.C) { |
|
497 |
+ c.Assert(s.d.Start(), checker.IsNil) |
|
498 |
+ c.Assert(s.ec.caps, checker.Equals, 0) |
|
499 |
+ |
|
500 |
+ for i := 0; i < 3; i++ { |
|
501 |
+ out, err := s.d.Cmd("volume", "create", "-d", "test-external-volume-driver", "--name", fmt.Sprintf("test%d", i)) |
|
502 |
+ c.Assert(err, checker.IsNil, check.Commentf(out)) |
|
503 |
+ c.Assert(s.ec.caps, checker.Equals, 1) |
|
504 |
+ out, err = s.d.Cmd("volume", "inspect", "--format={{.Scope}}", fmt.Sprintf("test%d", i)) |
|
505 |
+ c.Assert(err, checker.IsNil) |
|
506 |
+ c.Assert(strings.TrimSpace(out), checker.Equals, volume.GlobalScope) |
|
507 |
+ } |
|
508 |
+} |
... | ... |
@@ -78,7 +78,7 @@ func main() { |
78 | 78 |
|
79 | 79 |
errorOut("parser error", generatedTempl.Execute(&buf, analysis)) |
80 | 80 |
src, err := format.Source(buf.Bytes()) |
81 |
- errorOut("error formating generated source:\n"+buf.String(), err) |
|
81 |
+ errorOut("error formatting generated source:\n"+buf.String(), err) |
|
82 | 82 |
errorOut("error writing file", ioutil.WriteFile(*outputFile, src, 0644)) |
83 | 83 |
} |
84 | 84 |
|
... | ... |
@@ -1,14 +1,22 @@ |
1 | 1 |
package volumedrivers |
2 | 2 |
|
3 | 3 |
import ( |
4 |
- "fmt" |
|
4 |
+ "errors" |
|
5 |
+ "strings" |
|
5 | 6 |
|
7 |
+ "github.com/Sirupsen/logrus" |
|
6 | 8 |
"github.com/docker/docker/volume" |
7 | 9 |
) |
8 | 10 |
|
11 |
+var ( |
|
12 |
+ errInvalidScope = errors.New("invalid scope") |
|
13 |
+ errNoSuchVolume = errors.New("no such volume") |
|
14 |
+) |
|
15 |
+ |
|
9 | 16 |
type volumeDriverAdapter struct { |
10 |
- name string |
|
11 |
- proxy *volumeDriverProxy |
|
17 |
+ name string |
|
18 |
+ capabilities *volume.Capability |
|
19 |
+ proxy *volumeDriverProxy |
|
12 | 20 |
} |
13 | 21 |
|
14 | 22 |
func (a *volumeDriverAdapter) Name() string { |
... | ... |
@@ -56,7 +64,7 @@ func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) { |
56 | 56 |
|
57 | 57 |
// plugin may have returned no volume and no error |
58 | 58 |
if v == nil { |
59 |
- return nil, fmt.Errorf("no such volume") |
|
59 |
+ return nil, errNoSuchVolume |
|
60 | 60 |
} |
61 | 61 |
|
62 | 62 |
return &volumeAdapter{ |
... | ... |
@@ -68,6 +76,38 @@ func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) { |
68 | 68 |
}, nil |
69 | 69 |
} |
70 | 70 |
|
71 |
+func (a *volumeDriverAdapter) Scope() string { |
|
72 |
+ cap := a.getCapabilities() |
|
73 |
+ return cap.Scope |
|
74 |
+} |
|
75 |
+ |
|
76 |
+func (a *volumeDriverAdapter) getCapabilities() volume.Capability { |
|
77 |
+ if a.capabilities != nil { |
|
78 |
+ return *a.capabilities |
|
79 |
+ } |
|
80 |
+ cap, err := a.proxy.Capabilities() |
|
81 |
+ if err != nil { |
|
82 |
+ // `GetCapabilities` is a not a required endpoint. |
|
83 |
+ // On error assume it's a local-only driver |
|
84 |
+ logrus.Warnf("Volume driver %s returned an error while trying to query it's capabilities, using default capabilties: %v", a.name, err) |
|
85 |
+ return volume.Capability{Scope: volume.LocalScope} |
|
86 |
+ } |
|
87 |
+ |
|
88 |
+ // don't spam the warn log below just because the plugin didn't provide a scope |
|
89 |
+ if len(cap.Scope) == 0 { |
|
90 |
+ cap.Scope = volume.LocalScope |
|
91 |
+ } |
|
92 |
+ |
|
93 |
+ cap.Scope = strings.ToLower(cap.Scope) |
|
94 |
+ if cap.Scope != volume.LocalScope && cap.Scope != volume.GlobalScope { |
|
95 |
+ logrus.Warnf("Volume driver %q returned an invalid scope: %q", a.Name(), cap.Scope) |
|
96 |
+ cap.Scope = volume.LocalScope |
|
97 |
+ } |
|
98 |
+ |
|
99 |
+ a.capabilities = &cap |
|
100 |
+ return cap |
|
101 |
+} |
|
102 |
+ |
|
71 | 103 |
type volumeAdapter struct { |
72 | 104 |
proxy *volumeDriverProxy |
73 | 105 |
name string |
... | ... |
@@ -42,6 +42,8 @@ type volumeDriver interface { |
42 | 42 |
List() (volumes []*proxyVolume, err error) |
43 | 43 |
// Get retrieves the volume with the requested name |
44 | 44 |
Get(name string) (volume *proxyVolume, err error) |
45 |
+ // Capabilities gets the list of capabilities of the driver |
|
46 |
+ Capabilities() (capabilities volume.Capability, err error) |
|
45 | 47 |
} |
46 | 48 |
|
47 | 49 |
type driverExtpoint struct { |
... | ... |
@@ -64,6 +66,11 @@ func Register(extension volume.Driver, name string) bool { |
64 | 64 |
if exists { |
65 | 65 |
return false |
66 | 66 |
} |
67 |
+ |
|
68 |
+ if err := validateDriver(extension); err != nil { |
|
69 |
+ return false |
|
70 |
+ } |
|
71 |
+ |
|
67 | 72 |
drivers.extensions[name] = extension |
68 | 73 |
return true |
69 | 74 |
} |
... | ... |
@@ -107,10 +114,22 @@ func Lookup(name string) (volume.Driver, error) { |
107 | 107 |
} |
108 | 108 |
|
109 | 109 |
d := NewVolumeDriver(name, pl.Client) |
110 |
+ if err := validateDriver(d); err != nil { |
|
111 |
+ return nil, err |
|
112 |
+ } |
|
113 |
+ |
|
110 | 114 |
drivers.extensions[name] = d |
111 | 115 |
return d, nil |
112 | 116 |
} |
113 | 117 |
|
118 |
+func validateDriver(vd volume.Driver) error { |
|
119 |
+ scope := vd.Scope() |
|
120 |
+ if scope != volume.LocalScope && scope != volume.GlobalScope { |
|
121 |
+ return fmt.Errorf("Driver %q provided an invalid capability scope: %s", vd.Name(), scope) |
|
122 |
+ } |
|
123 |
+ return nil |
|
124 |
+} |
|
125 |
+ |
|
114 | 126 |
// GetDriver returns a volume driver by its name. |
115 | 127 |
// If the driver is empty, it looks for the local driver. |
116 | 128 |
func GetDriver(name string) (volume.Driver, error) { |
... | ... |
@@ -2,7 +2,10 @@ |
2 | 2 |
|
3 | 3 |
package volumedrivers |
4 | 4 |
|
5 |
-import "errors" |
|
5 |
+import ( |
|
6 |
+ "errors" |
|
7 |
+ "github.com/docker/docker/volume" |
|
8 |
+) |
|
6 | 9 |
|
7 | 10 |
type client interface { |
8 | 11 |
Call(string, interface{}, interface{}) error |
... | ... |
@@ -209,3 +212,30 @@ func (pp *volumeDriverProxy) Get(name string) (volume *proxyVolume, err error) { |
209 | 209 |
|
210 | 210 |
return |
211 | 211 |
} |
212 |
+ |
|
213 |
+type volumeDriverProxyCapabilitiesRequest struct { |
|
214 |
+} |
|
215 |
+ |
|
216 |
+type volumeDriverProxyCapabilitiesResponse struct { |
|
217 |
+ Capabilities volume.Capability |
|
218 |
+ Err string |
|
219 |
+} |
|
220 |
+ |
|
221 |
+func (pp *volumeDriverProxy) Capabilities() (capabilities volume.Capability, err error) { |
|
222 |
+ var ( |
|
223 |
+ req volumeDriverProxyCapabilitiesRequest |
|
224 |
+ ret volumeDriverProxyCapabilitiesResponse |
|
225 |
+ ) |
|
226 |
+ |
|
227 |
+ if err = pp.Call("VolumeDriver.Capabilities", req, &ret); err != nil { |
|
228 |
+ return |
|
229 |
+ } |
|
230 |
+ |
|
231 |
+ capabilities = ret.Capabilities |
|
232 |
+ |
|
233 |
+ if ret.Err != "" { |
|
234 |
+ err = errors.New(ret.Err) |
|
235 |
+ } |
|
236 |
+ |
|
237 |
+ return |
|
238 |
+} |
... | ... |
@@ -52,6 +52,11 @@ func TestVolumeRequestError(t *testing.T) { |
52 | 52 |
fmt.Fprintln(w, `{"Err": "Cannot get volume"}`) |
53 | 53 |
}) |
54 | 54 |
|
55 |
+ mux.HandleFunc("/VolumeDriver.Capabilities", func(w http.ResponseWriter, r *http.Request) { |
|
56 |
+ w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") |
|
57 |
+ http.Error(w, "error", 500) |
|
58 |
+ }) |
|
59 |
+ |
|
55 | 60 |
u, _ := url.Parse(server.URL) |
56 | 61 |
client, err := plugins.NewClient("tcp://"+u.Host, tlsconfig.Options{InsecureSkipVerify: true}) |
57 | 62 |
if err != nil { |
... | ... |
@@ -119,4 +124,9 @@ func TestVolumeRequestError(t *testing.T) { |
119 | 119 |
if !strings.Contains(err.Error(), "Cannot get volume") { |
120 | 120 |
t.Fatalf("Unexpected error: %v\n", err) |
121 | 121 |
} |
122 |
+ |
|
123 |
+ _, err = driver.Capabilities() |
|
124 |
+ if err == nil { |
|
125 |
+ t.Fatal(err) |
|
126 |
+ } |
|
122 | 127 |
} |
... | ... |
@@ -248,6 +248,11 @@ func (r *Root) Get(name string) (volume.Volume, error) { |
248 | 248 |
return v, nil |
249 | 249 |
} |
250 | 250 |
|
251 |
+// Scope returns the local volume scope |
|
252 |
+func (r *Root) Scope() string { |
|
253 |
+ return volume.LocalScope |
|
254 |
+} |
|
255 |
+ |
|
251 | 256 |
func (r *Root) validateName(name string) error { |
252 | 257 |
if !volumeNameRegex.MatchString(name) { |
253 | 258 |
return validationError{fmt.Errorf("%q includes invalid characters for a local volume name, only %q are allowed", name, utils.RestrictedNameChars)} |
... | ... |
@@ -25,15 +25,29 @@ type volumeMetadata struct { |
25 | 25 |
Labels map[string]string |
26 | 26 |
} |
27 | 27 |
|
28 |
-type volumeWithLabels struct { |
|
28 |
+type volumeWrapper struct { |
|
29 | 29 |
volume.Volume |
30 | 30 |
labels map[string]string |
31 |
+ scope string |
|
31 | 32 |
} |
32 | 33 |
|
33 |
-func (v volumeWithLabels) Labels() map[string]string { |
|
34 |
+func (v volumeWrapper) Labels() map[string]string { |
|
34 | 35 |
return v.labels |
35 | 36 |
} |
36 | 37 |
|
38 |
+func (v volumeWrapper) Scope() string { |
|
39 |
+ return v.scope |
|
40 |
+} |
|
41 |
+ |
|
42 |
+func (v volumeWrapper) CachedPath() string { |
|
43 |
+ if vv, ok := v.Volume.(interface { |
|
44 |
+ CachedPath() string |
|
45 |
+ }); ok { |
|
46 |
+ return vv.CachedPath() |
|
47 |
+ } |
|
48 |
+ return v.Volume.Path() |
|
49 |
+} |
|
50 |
+ |
|
37 | 51 |
// New initializes a VolumeStore to keep |
38 | 52 |
// reference counting of volumes in the system. |
39 | 53 |
func New(rootPath string) (*VolumeStore, error) { |
... | ... |
@@ -166,6 +180,10 @@ func (s *VolumeStore) list() ([]volume.Volume, []string, error) { |
166 | 166 |
chVols <- vols{driverName: d.Name(), err: &OpErr{Err: err, Name: d.Name(), Op: "list"}} |
167 | 167 |
return |
168 | 168 |
} |
169 |
+ for i, v := range vs { |
|
170 |
+ vs[i] = volumeWrapper{v, s.labels[v.Name()], d.Scope()} |
|
171 |
+ } |
|
172 |
+ |
|
169 | 173 |
chVols <- vols{vols: vs} |
170 | 174 |
}(vd) |
171 | 175 |
} |
... | ... |
@@ -291,7 +309,7 @@ func (s *VolumeStore) create(name, driverName string, opts, labels map[string]st |
291 | 291 |
} |
292 | 292 |
} |
293 | 293 |
|
294 |
- return volumeWithLabels{v, labels}, nil |
|
294 |
+ return volumeWrapper{v, labels, vd.Scope()}, nil |
|
295 | 295 |
} |
296 | 296 |
|
297 | 297 |
// GetWithRef gets a volume with the given name from the passed in driver and stores the ref |
... | ... |
@@ -313,10 +331,8 @@ func (s *VolumeStore) GetWithRef(name, driverName, ref string) (volume.Volume, e |
313 | 313 |
} |
314 | 314 |
|
315 | 315 |
s.setNamed(v, ref) |
316 |
- if labels, ok := s.labels[name]; ok { |
|
317 |
- return volumeWithLabels{v, labels}, nil |
|
318 |
- } |
|
319 |
- return v, nil |
|
316 |
+ |
|
317 |
+ return volumeWrapper{v, s.labels[name], vd.Scope()}, nil |
|
320 | 318 |
} |
321 | 319 |
|
322 | 320 |
// Get looks if a volume with the given name exists and returns it if so |
... | ... |
@@ -376,7 +392,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) { |
376 | 376 |
if err != nil { |
377 | 377 |
return nil, err |
378 | 378 |
} |
379 |
- return volumeWithLabels{vol, labels}, nil |
|
379 |
+ return volumeWrapper{vol, labels, vd.Scope()}, nil |
|
380 | 380 |
} |
381 | 381 |
|
382 | 382 |
logrus.Debugf("Probing all drivers for volume with name: %s", name) |
... | ... |
@@ -391,7 +407,7 @@ func (s *VolumeStore) getVolume(name string) (volume.Volume, error) { |
391 | 391 |
continue |
392 | 392 |
} |
393 | 393 |
|
394 |
- return volumeWithLabels{v, labels}, nil |
|
394 |
+ return volumeWrapper{v, labels, d.Scope()}, nil |
|
395 | 395 |
} |
396 | 396 |
return nil, errNoSuchVolume |
397 | 397 |
} |
... | ... |
@@ -412,7 +428,7 @@ func (s *VolumeStore) Remove(v volume.Volume) error { |
412 | 412 |
} |
413 | 413 |
|
414 | 414 |
logrus.Debugf("Removing volume reference: driver %s, name %s", v.DriverName(), name) |
415 |
- vol := withoutLabels(v) |
|
415 |
+ vol := unwrapVolume(v) |
|
416 | 416 |
if err := vd.Remove(vol); err != nil { |
417 | 417 |
return &OpErr{Err: err, Name: name, Op: "remove"} |
418 | 418 |
} |
... | ... |
@@ -465,6 +481,9 @@ func (s *VolumeStore) FilterByDriver(name string) ([]volume.Volume, error) { |
465 | 465 |
if err != nil { |
466 | 466 |
return nil, &OpErr{Err: err, Name: name, Op: "list"} |
467 | 467 |
} |
468 |
+ for i, v := range ls { |
|
469 |
+ ls[i] = volumeWrapper{v, s.labels[v.Name()], vd.Scope()} |
|
470 |
+ } |
|
468 | 471 |
return ls, nil |
469 | 472 |
} |
470 | 473 |
|
... | ... |
@@ -497,8 +516,8 @@ func (s *VolumeStore) filter(vols []volume.Volume, f filterFunc) []volume.Volume |
497 | 497 |
return ls |
498 | 498 |
} |
499 | 499 |
|
500 |
-func withoutLabels(v volume.Volume) volume.Volume { |
|
501 |
- if vol, ok := v.(volumeWithLabels); ok { |
|
500 |
+func unwrapVolume(v volume.Volume) volume.Volume { |
|
501 |
+ if vol, ok := v.(volumeWrapper); ok { |
|
502 | 502 |
return vol.Volume |
503 | 503 |
} |
504 | 504 |
|
... | ... |
@@ -13,7 +13,14 @@ import ( |
13 | 13 |
|
14 | 14 |
// DefaultDriverName is the driver name used for the driver |
15 | 15 |
// implemented in the local package. |
16 |
-const DefaultDriverName string = "local" |
|
16 |
+const DefaultDriverName = "local" |
|
17 |
+ |
|
18 |
+// Scopes define if a volume has is cluster-wide (global) or local only. |
|
19 |
+// Scopes are returned by the volume driver when it is queried for capabilities and then set on a volume |
|
20 |
+const ( |
|
21 |
+ LocalScope = "local" |
|
22 |
+ GlobalScope = "global" |
|
23 |
+) |
|
17 | 24 |
|
18 | 25 |
// Driver is for creating and removing volumes. |
19 | 26 |
type Driver interface { |
... | ... |
@@ -27,6 +34,18 @@ type Driver interface { |
27 | 27 |
List() ([]Volume, error) |
28 | 28 |
// Get retrieves the volume with the requested name |
29 | 29 |
Get(name string) (Volume, error) |
30 |
+ // Scope returns the scope of the driver (e.g. `golbal` or `local`). |
|
31 |
+ // Scope determines how the driver is handled at a cluster level |
|
32 |
+ Scope() string |
|
33 |
+} |
|
34 |
+ |
|
35 |
+// Capability defines a set of capabilities that a driver is able to handle. |
|
36 |
+type Capability struct { |
|
37 |
+ // Scope is the scope of the driver, `global` or `local` |
|
38 |
+ // A `global` scope indicates that the driver manages volumes across the cluster |
|
39 |
+ // A `local` scope indicates that the driver only manages volumes resources local to the host |
|
40 |
+ // Scope is declared by the driver |
|
41 |
+ Scope string |
|
30 | 42 |
} |
31 | 43 |
|
32 | 44 |
// Volume is a place to store data. It is backed by a specific driver, and can be mounted. |
... | ... |
@@ -46,6 +65,18 @@ type Volume interface { |
46 | 46 |
Status() map[string]interface{} |
47 | 47 |
} |
48 | 48 |
|
49 |
+// LabeledVolume wraps a Volume with user-defined labels |
|
50 |
+type LabeledVolume interface { |
|
51 |
+ Labels() map[string]string |
|
52 |
+ Volume |
|
53 |
+} |
|
54 |
+ |
|
55 |
+// ScopedVolume wraps a volume with a cluster scope (e.g., `local` or `global`) |
|
56 |
+type ScopedVolume interface { |
|
57 |
+ Scope() string |
|
58 |
+ Volume |
|
59 |
+} |
|
60 |
+ |
|
49 | 61 |
// MountPoint is the intersection point between a volume and a container. It |
50 | 62 |
// specifies which volume is to be used and where inside a container it should |
51 | 63 |
// be mounted. |