Browse code

Make plugin emit strongly typed, consumable events

Enables other subsystems to watch actions for a plugin(s).

This will be used specifically for implementing plugins on swarm where a
swarm controller needs to watch the state of a plugin.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff authored on 2017/06/08 02:07:01
Showing 37 changed files
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	"github.com/docker/distribution/reference"
8 8
 	enginetypes "github.com/docker/docker/api/types"
9 9
 	"github.com/docker/docker/api/types/filters"
10
+	"github.com/docker/docker/plugin"
10 11
 	"golang.org/x/net/context"
11 12
 )
12 13
 
... ...
@@ -19,7 +20,7 @@ type Backend interface {
19 19
 	Remove(name string, config *enginetypes.PluginRmConfig) error
20 20
 	Set(name string, args []string) error
21 21
 	Privileges(ctx context.Context, ref reference.Named, metaHeaders http.Header, authConfig *enginetypes.AuthConfig) (enginetypes.PluginPrivileges, error)
22
-	Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
22
+	Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer, opts ...plugin.CreateOpt) error
23 23
 	Push(ctx context.Context, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, outStream io.Writer) error
24 24
 	Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
25 25
 	CreateFromContext(ctx context.Context, tarCtx io.ReadCloser, options *enginetypes.PluginCreateOptions) error
... ...
@@ -44,7 +44,7 @@ func (sr *swarmRouter) swarmLogs(ctx context.Context, w http.ResponseWriter, r *
44 44
 			// maybe should return some context with this error?
45 45
 			return err
46 46
 		}
47
-		tty = s.Spec.TaskTemplate.ContainerSpec.TTY || tty
47
+		tty = (s.Spec.TaskTemplate.ContainerSpec != nil && s.Spec.TaskTemplate.ContainerSpec.TTY) || tty
48 48
 	}
49 49
 	for _, task := range selector.Tasks {
50 50
 		t, err := sr.backend.GetTask(task)
... ...
@@ -1975,11 +1975,39 @@ definitions:
1975 1975
     description: "User modifiable task configuration."
1976 1976
     type: "object"
1977 1977
     properties:
1978
+      PluginSpec:
1979
+        type: "object"
1980
+        description: "Invalid when specified with `ContainerSpec`."
1981
+        properties:
1982
+          Name:
1983
+            description: "The name or 'alias' to use for the plugin."
1984
+            type: "string"
1985
+          Remote:
1986
+            description: "The plugin image reference to use."
1987
+            type: "string"
1988
+          Disabled:
1989
+            description: "Disable the plugin once scheduled."
1990
+            type: "boolean"
1991
+          PluginPrivilege:
1992
+            type: "array"
1993
+            items:
1994
+              description: "Describes a permission accepted by the user upon installing the plugin."
1995
+              type: "object"
1996
+              properties:
1997
+                Name:
1998
+                  type: "string"
1999
+                Description:
2000
+                  type: "string"
2001
+                Value:
2002
+                  type: "array"
2003
+                  items:
2004
+                    type: "string"
1978 2005
       ContainerSpec:
1979 2006
         type: "object"
2007
+        description: "Invalid when specified with `PluginSpec`."
1980 2008
         properties:
1981 2009
           Image:
1982
-            description: "The image name to use for the container."
2010
+            description: "The image name to use for the container"
1983 2011
             type: "string"
1984 2012
           Labels:
1985 2013
             description: "User-defined key/value data."
1986 2014
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+//go:generate protoc -I . --gogofast_out=import_path=github.com/docker/docker/api/types/swarm/runtime:. plugin.proto
1
+
2
+package runtime
0 3
new file mode 100644
... ...
@@ -0,0 +1,712 @@
0
+// Code generated by protoc-gen-gogo.
1
+// source: plugin.proto
2
+// DO NOT EDIT!
3
+
4
+/*
5
+	Package runtime is a generated protocol buffer package.
6
+
7
+	It is generated from these files:
8
+		plugin.proto
9
+
10
+	It has these top-level messages:
11
+		PluginSpec
12
+		PluginPrivilege
13
+*/
14
+package runtime
15
+
16
+import proto "github.com/gogo/protobuf/proto"
17
+import fmt "fmt"
18
+import math "math"
19
+
20
+import io "io"
21
+
22
+// Reference imports to suppress errors if they are not otherwise used.
23
+var _ = proto.Marshal
24
+var _ = fmt.Errorf
25
+var _ = math.Inf
26
+
27
+// This is a compile-time assertion to ensure that this generated file
28
+// is compatible with the proto package it is being compiled against.
29
+// A compilation error at this line likely means your copy of the
30
+// proto package needs to be updated.
31
+const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
32
+
33
+// PluginSpec defines the base payload which clients can specify for creating
34
+// a service with the plugin runtime.
35
+type PluginSpec struct {
36
+	Name       string             `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
37
+	Remote     string             `protobuf:"bytes,2,opt,name=remote,proto3" json:"remote,omitempty"`
38
+	Privileges []*PluginPrivilege `protobuf:"bytes,3,rep,name=privileges" json:"privileges,omitempty"`
39
+	Disabled   bool               `protobuf:"varint,4,opt,name=disabled,proto3" json:"disabled,omitempty"`
40
+}
41
+
42
+func (m *PluginSpec) Reset()                    { *m = PluginSpec{} }
43
+func (m *PluginSpec) String() string            { return proto.CompactTextString(m) }
44
+func (*PluginSpec) ProtoMessage()               {}
45
+func (*PluginSpec) Descriptor() ([]byte, []int) { return fileDescriptorPlugin, []int{0} }
46
+
47
+func (m *PluginSpec) GetName() string {
48
+	if m != nil {
49
+		return m.Name
50
+	}
51
+	return ""
52
+}
53
+
54
+func (m *PluginSpec) GetRemote() string {
55
+	if m != nil {
56
+		return m.Remote
57
+	}
58
+	return ""
59
+}
60
+
61
+func (m *PluginSpec) GetPrivileges() []*PluginPrivilege {
62
+	if m != nil {
63
+		return m.Privileges
64
+	}
65
+	return nil
66
+}
67
+
68
+func (m *PluginSpec) GetDisabled() bool {
69
+	if m != nil {
70
+		return m.Disabled
71
+	}
72
+	return false
73
+}
74
+
75
+// PluginPrivilege describes a permission the user has to accept
76
+// upon installing a plugin.
77
+type PluginPrivilege struct {
78
+	Name        string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
79
+	Description string   `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
80
+	Value       []string `protobuf:"bytes,3,rep,name=value" json:"value,omitempty"`
81
+}
82
+
83
+func (m *PluginPrivilege) Reset()                    { *m = PluginPrivilege{} }
84
+func (m *PluginPrivilege) String() string            { return proto.CompactTextString(m) }
85
+func (*PluginPrivilege) ProtoMessage()               {}
86
+func (*PluginPrivilege) Descriptor() ([]byte, []int) { return fileDescriptorPlugin, []int{1} }
87
+
88
+func (m *PluginPrivilege) GetName() string {
89
+	if m != nil {
90
+		return m.Name
91
+	}
92
+	return ""
93
+}
94
+
95
+func (m *PluginPrivilege) GetDescription() string {
96
+	if m != nil {
97
+		return m.Description
98
+	}
99
+	return ""
100
+}
101
+
102
+func (m *PluginPrivilege) GetValue() []string {
103
+	if m != nil {
104
+		return m.Value
105
+	}
106
+	return nil
107
+}
108
+
109
+func init() {
110
+	proto.RegisterType((*PluginSpec)(nil), "PluginSpec")
111
+	proto.RegisterType((*PluginPrivilege)(nil), "PluginPrivilege")
112
+}
113
+func (m *PluginSpec) Marshal() (dAtA []byte, err error) {
114
+	size := m.Size()
115
+	dAtA = make([]byte, size)
116
+	n, err := m.MarshalTo(dAtA)
117
+	if err != nil {
118
+		return nil, err
119
+	}
120
+	return dAtA[:n], nil
121
+}
122
+
123
+func (m *PluginSpec) MarshalTo(dAtA []byte) (int, error) {
124
+	var i int
125
+	_ = i
126
+	var l int
127
+	_ = l
128
+	if len(m.Name) > 0 {
129
+		dAtA[i] = 0xa
130
+		i++
131
+		i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name)))
132
+		i += copy(dAtA[i:], m.Name)
133
+	}
134
+	if len(m.Remote) > 0 {
135
+		dAtA[i] = 0x12
136
+		i++
137
+		i = encodeVarintPlugin(dAtA, i, uint64(len(m.Remote)))
138
+		i += copy(dAtA[i:], m.Remote)
139
+	}
140
+	if len(m.Privileges) > 0 {
141
+		for _, msg := range m.Privileges {
142
+			dAtA[i] = 0x1a
143
+			i++
144
+			i = encodeVarintPlugin(dAtA, i, uint64(msg.Size()))
145
+			n, err := msg.MarshalTo(dAtA[i:])
146
+			if err != nil {
147
+				return 0, err
148
+			}
149
+			i += n
150
+		}
151
+	}
152
+	if m.Disabled {
153
+		dAtA[i] = 0x20
154
+		i++
155
+		if m.Disabled {
156
+			dAtA[i] = 1
157
+		} else {
158
+			dAtA[i] = 0
159
+		}
160
+		i++
161
+	}
162
+	return i, nil
163
+}
164
+
165
+func (m *PluginPrivilege) Marshal() (dAtA []byte, err error) {
166
+	size := m.Size()
167
+	dAtA = make([]byte, size)
168
+	n, err := m.MarshalTo(dAtA)
169
+	if err != nil {
170
+		return nil, err
171
+	}
172
+	return dAtA[:n], nil
173
+}
174
+
175
+func (m *PluginPrivilege) MarshalTo(dAtA []byte) (int, error) {
176
+	var i int
177
+	_ = i
178
+	var l int
179
+	_ = l
180
+	if len(m.Name) > 0 {
181
+		dAtA[i] = 0xa
182
+		i++
183
+		i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name)))
184
+		i += copy(dAtA[i:], m.Name)
185
+	}
186
+	if len(m.Description) > 0 {
187
+		dAtA[i] = 0x12
188
+		i++
189
+		i = encodeVarintPlugin(dAtA, i, uint64(len(m.Description)))
190
+		i += copy(dAtA[i:], m.Description)
191
+	}
192
+	if len(m.Value) > 0 {
193
+		for _, s := range m.Value {
194
+			dAtA[i] = 0x1a
195
+			i++
196
+			l = len(s)
197
+			for l >= 1<<7 {
198
+				dAtA[i] = uint8(uint64(l)&0x7f | 0x80)
199
+				l >>= 7
200
+				i++
201
+			}
202
+			dAtA[i] = uint8(l)
203
+			i++
204
+			i += copy(dAtA[i:], s)
205
+		}
206
+	}
207
+	return i, nil
208
+}
209
+
210
+func encodeFixed64Plugin(dAtA []byte, offset int, v uint64) int {
211
+	dAtA[offset] = uint8(v)
212
+	dAtA[offset+1] = uint8(v >> 8)
213
+	dAtA[offset+2] = uint8(v >> 16)
214
+	dAtA[offset+3] = uint8(v >> 24)
215
+	dAtA[offset+4] = uint8(v >> 32)
216
+	dAtA[offset+5] = uint8(v >> 40)
217
+	dAtA[offset+6] = uint8(v >> 48)
218
+	dAtA[offset+7] = uint8(v >> 56)
219
+	return offset + 8
220
+}
221
+func encodeFixed32Plugin(dAtA []byte, offset int, v uint32) int {
222
+	dAtA[offset] = uint8(v)
223
+	dAtA[offset+1] = uint8(v >> 8)
224
+	dAtA[offset+2] = uint8(v >> 16)
225
+	dAtA[offset+3] = uint8(v >> 24)
226
+	return offset + 4
227
+}
228
+func encodeVarintPlugin(dAtA []byte, offset int, v uint64) int {
229
+	for v >= 1<<7 {
230
+		dAtA[offset] = uint8(v&0x7f | 0x80)
231
+		v >>= 7
232
+		offset++
233
+	}
234
+	dAtA[offset] = uint8(v)
235
+	return offset + 1
236
+}
237
+func (m *PluginSpec) Size() (n int) {
238
+	var l int
239
+	_ = l
240
+	l = len(m.Name)
241
+	if l > 0 {
242
+		n += 1 + l + sovPlugin(uint64(l))
243
+	}
244
+	l = len(m.Remote)
245
+	if l > 0 {
246
+		n += 1 + l + sovPlugin(uint64(l))
247
+	}
248
+	if len(m.Privileges) > 0 {
249
+		for _, e := range m.Privileges {
250
+			l = e.Size()
251
+			n += 1 + l + sovPlugin(uint64(l))
252
+		}
253
+	}
254
+	if m.Disabled {
255
+		n += 2
256
+	}
257
+	return n
258
+}
259
+
260
+func (m *PluginPrivilege) Size() (n int) {
261
+	var l int
262
+	_ = l
263
+	l = len(m.Name)
264
+	if l > 0 {
265
+		n += 1 + l + sovPlugin(uint64(l))
266
+	}
267
+	l = len(m.Description)
268
+	if l > 0 {
269
+		n += 1 + l + sovPlugin(uint64(l))
270
+	}
271
+	if len(m.Value) > 0 {
272
+		for _, s := range m.Value {
273
+			l = len(s)
274
+			n += 1 + l + sovPlugin(uint64(l))
275
+		}
276
+	}
277
+	return n
278
+}
279
+
280
+func sovPlugin(x uint64) (n int) {
281
+	for {
282
+		n++
283
+		x >>= 7
284
+		if x == 0 {
285
+			break
286
+		}
287
+	}
288
+	return n
289
+}
290
+func sozPlugin(x uint64) (n int) {
291
+	return sovPlugin(uint64((x << 1) ^ uint64((int64(x) >> 63))))
292
+}
293
+func (m *PluginSpec) Unmarshal(dAtA []byte) error {
294
+	l := len(dAtA)
295
+	iNdEx := 0
296
+	for iNdEx < l {
297
+		preIndex := iNdEx
298
+		var wire uint64
299
+		for shift := uint(0); ; shift += 7 {
300
+			if shift >= 64 {
301
+				return ErrIntOverflowPlugin
302
+			}
303
+			if iNdEx >= l {
304
+				return io.ErrUnexpectedEOF
305
+			}
306
+			b := dAtA[iNdEx]
307
+			iNdEx++
308
+			wire |= (uint64(b) & 0x7F) << shift
309
+			if b < 0x80 {
310
+				break
311
+			}
312
+		}
313
+		fieldNum := int32(wire >> 3)
314
+		wireType := int(wire & 0x7)
315
+		if wireType == 4 {
316
+			return fmt.Errorf("proto: PluginSpec: wiretype end group for non-group")
317
+		}
318
+		if fieldNum <= 0 {
319
+			return fmt.Errorf("proto: PluginSpec: illegal tag %d (wire type %d)", fieldNum, wire)
320
+		}
321
+		switch fieldNum {
322
+		case 1:
323
+			if wireType != 2 {
324
+				return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
325
+			}
326
+			var stringLen uint64
327
+			for shift := uint(0); ; shift += 7 {
328
+				if shift >= 64 {
329
+					return ErrIntOverflowPlugin
330
+				}
331
+				if iNdEx >= l {
332
+					return io.ErrUnexpectedEOF
333
+				}
334
+				b := dAtA[iNdEx]
335
+				iNdEx++
336
+				stringLen |= (uint64(b) & 0x7F) << shift
337
+				if b < 0x80 {
338
+					break
339
+				}
340
+			}
341
+			intStringLen := int(stringLen)
342
+			if intStringLen < 0 {
343
+				return ErrInvalidLengthPlugin
344
+			}
345
+			postIndex := iNdEx + intStringLen
346
+			if postIndex > l {
347
+				return io.ErrUnexpectedEOF
348
+			}
349
+			m.Name = string(dAtA[iNdEx:postIndex])
350
+			iNdEx = postIndex
351
+		case 2:
352
+			if wireType != 2 {
353
+				return fmt.Errorf("proto: wrong wireType = %d for field Remote", wireType)
354
+			}
355
+			var stringLen uint64
356
+			for shift := uint(0); ; shift += 7 {
357
+				if shift >= 64 {
358
+					return ErrIntOverflowPlugin
359
+				}
360
+				if iNdEx >= l {
361
+					return io.ErrUnexpectedEOF
362
+				}
363
+				b := dAtA[iNdEx]
364
+				iNdEx++
365
+				stringLen |= (uint64(b) & 0x7F) << shift
366
+				if b < 0x80 {
367
+					break
368
+				}
369
+			}
370
+			intStringLen := int(stringLen)
371
+			if intStringLen < 0 {
372
+				return ErrInvalidLengthPlugin
373
+			}
374
+			postIndex := iNdEx + intStringLen
375
+			if postIndex > l {
376
+				return io.ErrUnexpectedEOF
377
+			}
378
+			m.Remote = string(dAtA[iNdEx:postIndex])
379
+			iNdEx = postIndex
380
+		case 3:
381
+			if wireType != 2 {
382
+				return fmt.Errorf("proto: wrong wireType = %d for field Privileges", wireType)
383
+			}
384
+			var msglen int
385
+			for shift := uint(0); ; shift += 7 {
386
+				if shift >= 64 {
387
+					return ErrIntOverflowPlugin
388
+				}
389
+				if iNdEx >= l {
390
+					return io.ErrUnexpectedEOF
391
+				}
392
+				b := dAtA[iNdEx]
393
+				iNdEx++
394
+				msglen |= (int(b) & 0x7F) << shift
395
+				if b < 0x80 {
396
+					break
397
+				}
398
+			}
399
+			if msglen < 0 {
400
+				return ErrInvalidLengthPlugin
401
+			}
402
+			postIndex := iNdEx + msglen
403
+			if postIndex > l {
404
+				return io.ErrUnexpectedEOF
405
+			}
406
+			m.Privileges = append(m.Privileges, &PluginPrivilege{})
407
+			if err := m.Privileges[len(m.Privileges)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
408
+				return err
409
+			}
410
+			iNdEx = postIndex
411
+		case 4:
412
+			if wireType != 0 {
413
+				return fmt.Errorf("proto: wrong wireType = %d for field Disabled", wireType)
414
+			}
415
+			var v int
416
+			for shift := uint(0); ; shift += 7 {
417
+				if shift >= 64 {
418
+					return ErrIntOverflowPlugin
419
+				}
420
+				if iNdEx >= l {
421
+					return io.ErrUnexpectedEOF
422
+				}
423
+				b := dAtA[iNdEx]
424
+				iNdEx++
425
+				v |= (int(b) & 0x7F) << shift
426
+				if b < 0x80 {
427
+					break
428
+				}
429
+			}
430
+			m.Disabled = bool(v != 0)
431
+		default:
432
+			iNdEx = preIndex
433
+			skippy, err := skipPlugin(dAtA[iNdEx:])
434
+			if err != nil {
435
+				return err
436
+			}
437
+			if skippy < 0 {
438
+				return ErrInvalidLengthPlugin
439
+			}
440
+			if (iNdEx + skippy) > l {
441
+				return io.ErrUnexpectedEOF
442
+			}
443
+			iNdEx += skippy
444
+		}
445
+	}
446
+
447
+	if iNdEx > l {
448
+		return io.ErrUnexpectedEOF
449
+	}
450
+	return nil
451
+}
452
+func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
453
+	l := len(dAtA)
454
+	iNdEx := 0
455
+	for iNdEx < l {
456
+		preIndex := iNdEx
457
+		var wire uint64
458
+		for shift := uint(0); ; shift += 7 {
459
+			if shift >= 64 {
460
+				return ErrIntOverflowPlugin
461
+			}
462
+			if iNdEx >= l {
463
+				return io.ErrUnexpectedEOF
464
+			}
465
+			b := dAtA[iNdEx]
466
+			iNdEx++
467
+			wire |= (uint64(b) & 0x7F) << shift
468
+			if b < 0x80 {
469
+				break
470
+			}
471
+		}
472
+		fieldNum := int32(wire >> 3)
473
+		wireType := int(wire & 0x7)
474
+		if wireType == 4 {
475
+			return fmt.Errorf("proto: PluginPrivilege: wiretype end group for non-group")
476
+		}
477
+		if fieldNum <= 0 {
478
+			return fmt.Errorf("proto: PluginPrivilege: illegal tag %d (wire type %d)", fieldNum, wire)
479
+		}
480
+		switch fieldNum {
481
+		case 1:
482
+			if wireType != 2 {
483
+				return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
484
+			}
485
+			var stringLen uint64
486
+			for shift := uint(0); ; shift += 7 {
487
+				if shift >= 64 {
488
+					return ErrIntOverflowPlugin
489
+				}
490
+				if iNdEx >= l {
491
+					return io.ErrUnexpectedEOF
492
+				}
493
+				b := dAtA[iNdEx]
494
+				iNdEx++
495
+				stringLen |= (uint64(b) & 0x7F) << shift
496
+				if b < 0x80 {
497
+					break
498
+				}
499
+			}
500
+			intStringLen := int(stringLen)
501
+			if intStringLen < 0 {
502
+				return ErrInvalidLengthPlugin
503
+			}
504
+			postIndex := iNdEx + intStringLen
505
+			if postIndex > l {
506
+				return io.ErrUnexpectedEOF
507
+			}
508
+			m.Name = string(dAtA[iNdEx:postIndex])
509
+			iNdEx = postIndex
510
+		case 2:
511
+			if wireType != 2 {
512
+				return fmt.Errorf("proto: wrong wireType = %d for field Description", wireType)
513
+			}
514
+			var stringLen uint64
515
+			for shift := uint(0); ; shift += 7 {
516
+				if shift >= 64 {
517
+					return ErrIntOverflowPlugin
518
+				}
519
+				if iNdEx >= l {
520
+					return io.ErrUnexpectedEOF
521
+				}
522
+				b := dAtA[iNdEx]
523
+				iNdEx++
524
+				stringLen |= (uint64(b) & 0x7F) << shift
525
+				if b < 0x80 {
526
+					break
527
+				}
528
+			}
529
+			intStringLen := int(stringLen)
530
+			if intStringLen < 0 {
531
+				return ErrInvalidLengthPlugin
532
+			}
533
+			postIndex := iNdEx + intStringLen
534
+			if postIndex > l {
535
+				return io.ErrUnexpectedEOF
536
+			}
537
+			m.Description = string(dAtA[iNdEx:postIndex])
538
+			iNdEx = postIndex
539
+		case 3:
540
+			if wireType != 2 {
541
+				return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType)
542
+			}
543
+			var stringLen uint64
544
+			for shift := uint(0); ; shift += 7 {
545
+				if shift >= 64 {
546
+					return ErrIntOverflowPlugin
547
+				}
548
+				if iNdEx >= l {
549
+					return io.ErrUnexpectedEOF
550
+				}
551
+				b := dAtA[iNdEx]
552
+				iNdEx++
553
+				stringLen |= (uint64(b) & 0x7F) << shift
554
+				if b < 0x80 {
555
+					break
556
+				}
557
+			}
558
+			intStringLen := int(stringLen)
559
+			if intStringLen < 0 {
560
+				return ErrInvalidLengthPlugin
561
+			}
562
+			postIndex := iNdEx + intStringLen
563
+			if postIndex > l {
564
+				return io.ErrUnexpectedEOF
565
+			}
566
+			m.Value = append(m.Value, string(dAtA[iNdEx:postIndex]))
567
+			iNdEx = postIndex
568
+		default:
569
+			iNdEx = preIndex
570
+			skippy, err := skipPlugin(dAtA[iNdEx:])
571
+			if err != nil {
572
+				return err
573
+			}
574
+			if skippy < 0 {
575
+				return ErrInvalidLengthPlugin
576
+			}
577
+			if (iNdEx + skippy) > l {
578
+				return io.ErrUnexpectedEOF
579
+			}
580
+			iNdEx += skippy
581
+		}
582
+	}
583
+
584
+	if iNdEx > l {
585
+		return io.ErrUnexpectedEOF
586
+	}
587
+	return nil
588
+}
589
+func skipPlugin(dAtA []byte) (n int, err error) {
590
+	l := len(dAtA)
591
+	iNdEx := 0
592
+	for iNdEx < l {
593
+		var wire uint64
594
+		for shift := uint(0); ; shift += 7 {
595
+			if shift >= 64 {
596
+				return 0, ErrIntOverflowPlugin
597
+			}
598
+			if iNdEx >= l {
599
+				return 0, io.ErrUnexpectedEOF
600
+			}
601
+			b := dAtA[iNdEx]
602
+			iNdEx++
603
+			wire |= (uint64(b) & 0x7F) << shift
604
+			if b < 0x80 {
605
+				break
606
+			}
607
+		}
608
+		wireType := int(wire & 0x7)
609
+		switch wireType {
610
+		case 0:
611
+			for shift := uint(0); ; shift += 7 {
612
+				if shift >= 64 {
613
+					return 0, ErrIntOverflowPlugin
614
+				}
615
+				if iNdEx >= l {
616
+					return 0, io.ErrUnexpectedEOF
617
+				}
618
+				iNdEx++
619
+				if dAtA[iNdEx-1] < 0x80 {
620
+					break
621
+				}
622
+			}
623
+			return iNdEx, nil
624
+		case 1:
625
+			iNdEx += 8
626
+			return iNdEx, nil
627
+		case 2:
628
+			var length int
629
+			for shift := uint(0); ; shift += 7 {
630
+				if shift >= 64 {
631
+					return 0, ErrIntOverflowPlugin
632
+				}
633
+				if iNdEx >= l {
634
+					return 0, io.ErrUnexpectedEOF
635
+				}
636
+				b := dAtA[iNdEx]
637
+				iNdEx++
638
+				length |= (int(b) & 0x7F) << shift
639
+				if b < 0x80 {
640
+					break
641
+				}
642
+			}
643
+			iNdEx += length
644
+			if length < 0 {
645
+				return 0, ErrInvalidLengthPlugin
646
+			}
647
+			return iNdEx, nil
648
+		case 3:
649
+			for {
650
+				var innerWire uint64
651
+				var start int = iNdEx
652
+				for shift := uint(0); ; shift += 7 {
653
+					if shift >= 64 {
654
+						return 0, ErrIntOverflowPlugin
655
+					}
656
+					if iNdEx >= l {
657
+						return 0, io.ErrUnexpectedEOF
658
+					}
659
+					b := dAtA[iNdEx]
660
+					iNdEx++
661
+					innerWire |= (uint64(b) & 0x7F) << shift
662
+					if b < 0x80 {
663
+						break
664
+					}
665
+				}
666
+				innerWireType := int(innerWire & 0x7)
667
+				if innerWireType == 4 {
668
+					break
669
+				}
670
+				next, err := skipPlugin(dAtA[start:])
671
+				if err != nil {
672
+					return 0, err
673
+				}
674
+				iNdEx = start + next
675
+			}
676
+			return iNdEx, nil
677
+		case 4:
678
+			return iNdEx, nil
679
+		case 5:
680
+			iNdEx += 4
681
+			return iNdEx, nil
682
+		default:
683
+			return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
684
+		}
685
+	}
686
+	panic("unreachable")
687
+}
688
+
689
+var (
690
+	ErrInvalidLengthPlugin = fmt.Errorf("proto: negative length found during unmarshaling")
691
+	ErrIntOverflowPlugin   = fmt.Errorf("proto: integer overflow")
692
+)
693
+
694
+func init() { proto.RegisterFile("plugin.proto", fileDescriptorPlugin) }
695
+
696
+var fileDescriptorPlugin = []byte{
697
+	// 196 bytes of a gzipped FileDescriptorProto
698
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0xc8, 0x29, 0x4d,
699
+	0xcf, 0xcc, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x6a, 0x63, 0xe4, 0xe2, 0x0a, 0x00, 0x0b,
700
+	0x04, 0x17, 0xa4, 0x26, 0x0b, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30,
701
+	0x6a, 0x70, 0x06, 0x81, 0xd9, 0x42, 0x62, 0x5c, 0x6c, 0x45, 0xa9, 0xb9, 0xf9, 0x25, 0xa9, 0x12,
702
+	0x4c, 0x60, 0x51, 0x28, 0x4f, 0xc8, 0x80, 0x8b, 0xab, 0xa0, 0x28, 0xb3, 0x2c, 0x33, 0x27, 0x35,
703
+	0x3d, 0xb5, 0x58, 0x82, 0x59, 0x81, 0x59, 0x83, 0xdb, 0x48, 0x40, 0x0f, 0x62, 0x58, 0x00, 0x4c,
704
+	0x22, 0x08, 0x49, 0x8d, 0x90, 0x14, 0x17, 0x47, 0x4a, 0x66, 0x71, 0x62, 0x52, 0x4e, 0x6a, 0x8a,
705
+	0x04, 0x8b, 0x02, 0xa3, 0x06, 0x47, 0x10, 0x9c, 0xaf, 0x14, 0xcb, 0xc5, 0x8f, 0xa6, 0x15, 0xab,
706
+	0x63, 0x14, 0xb8, 0xb8, 0x53, 0x52, 0x8b, 0x93, 0x8b, 0x32, 0x0b, 0x4a, 0x32, 0xf3, 0xf3, 0xa0,
707
+	0x2e, 0x42, 0x16, 0x12, 0x12, 0xe1, 0x62, 0x2d, 0x4b, 0xcc, 0x29, 0x4d, 0x05, 0xbb, 0x88, 0x33,
708
+	0x08, 0xc2, 0x71, 0xe2, 0x39, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4,
709
+	0x18, 0x93, 0xd8, 0xc0, 0x9e, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb8, 0x84, 0xad, 0x79,
710
+	0x0c, 0x01, 0x00, 0x00,
711
+}
0 712
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+syntax = "proto3";
1
+
2
+// PluginSpec defines the base payload which clients can specify for creating
3
+// a service with the plugin runtime.
4
+message PluginSpec {
5
+	string name = 1;
6
+	string remote = 2;
7
+	repeated PluginPrivilege privileges = 3;
8
+	bool disabled = 4;
9
+}
10
+
11
+// PluginPrivilege describes a permission the user has to accept
12
+// upon installing a plugin.
13
+message PluginPrivilege {
14
+	string name = 1;
15
+	string description = 2;
16
+	repeated string value = 3;
17
+}
... ...
@@ -1,6 +1,10 @@
1 1
 package swarm
2 2
 
3
-import "time"
3
+import (
4
+	"time"
5
+
6
+	"github.com/docker/docker/api/types/swarm/runtime"
7
+)
4 8
 
5 9
 // TaskState represents the state of a task.
6 10
 type TaskState string
... ...
@@ -51,7 +55,11 @@ type Task struct {
51 51
 
52 52
 // TaskSpec represents the spec of a task.
53 53
 type TaskSpec struct {
54
-	ContainerSpec ContainerSpec             `json:",omitempty"`
54
+	// ContainerSpec and PluginSpec are mutually exclusive.
55
+	// PluginSpec will only be used when the `Runtime` field is set to `plugin`
56
+	ContainerSpec *ContainerSpec      `json:",omitempty"`
57
+	PluginSpec    *runtime.PluginSpec `json:",omitempty"`
58
+
55 59
 	Resources     *ResourceRequirements     `json:",omitempty"`
56 60
 	RestartPolicy *RestartPolicy            `json:",omitempty"`
57 61
 	Placement     *Placement                `json:",omitempty"`
... ...
@@ -6,9 +6,9 @@ import (
6 6
 
7 7
 	"github.com/docker/distribution/reference"
8 8
 	"github.com/docker/docker/api/types"
9
-	registrytypes "github.com/docker/docker/api/types/registry"
10 9
 	"github.com/docker/docker/api/types/swarm"
11 10
 	"github.com/opencontainers/go-digest"
11
+	"github.com/pkg/errors"
12 12
 	"golang.org/x/net/context"
13 13
 )
14 14
 
... ...
@@ -24,24 +24,51 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
24 24
 		headers["X-Registry-Auth"] = []string{options.EncodedRegistryAuth}
25 25
 	}
26 26
 
27
-	// ensure that the image is tagged
28
-	if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
29
-		service.TaskTemplate.ContainerSpec.Image = taggedImg
27
+	// Make sure containerSpec is not nil when no runtime is set or the runtime is set to container
28
+	if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
29
+		service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
30
+	}
31
+
32
+	if err := validateServiceSpec(service); err != nil {
33
+		return types.ServiceCreateResponse{}, err
30 34
 	}
31 35
 
32
-	// Contact the registry to retrieve digest and platform information
33
-	if options.QueryRegistry {
34
-		distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth)
35
-		distErr = err
36
-		if err == nil {
37
-			// now pin by digest if the image doesn't already contain a digest
38
-			if img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest); img != "" {
36
+	// ensure that the image is tagged
37
+	var imgPlatforms []swarm.Platform
38
+	if service.TaskTemplate.ContainerSpec != nil {
39
+		if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
40
+			service.TaskTemplate.ContainerSpec.Image = taggedImg
41
+		}
42
+		if options.QueryRegistry {
43
+			var img string
44
+			img, imgPlatforms, distErr = imageDigestAndPlatforms(ctx, cli, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth)
45
+			if img != "" {
39 46
 				service.TaskTemplate.ContainerSpec.Image = img
40 47
 			}
41
-			// add platforms that are compatible with the service
42
-			service.TaskTemplate.Placement = setServicePlatforms(service.TaskTemplate.Placement, distributionInspect)
43 48
 		}
44 49
 	}
50
+
51
+	// ensure that the image is tagged
52
+	if service.TaskTemplate.PluginSpec != nil {
53
+		if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
54
+			service.TaskTemplate.PluginSpec.Remote = taggedImg
55
+		}
56
+		if options.QueryRegistry {
57
+			var img string
58
+			img, imgPlatforms, distErr = imageDigestAndPlatforms(ctx, cli, service.TaskTemplate.PluginSpec.Remote, options.EncodedRegistryAuth)
59
+			if img != "" {
60
+				service.TaskTemplate.PluginSpec.Remote = img
61
+			}
62
+		}
63
+	}
64
+
65
+	if service.TaskTemplate.Placement == nil && len(imgPlatforms) > 0 {
66
+		service.TaskTemplate.Placement = &swarm.Placement{}
67
+	}
68
+	if len(imgPlatforms) > 0 {
69
+		service.TaskTemplate.Placement.Platforms = imgPlatforms
70
+	}
71
+
45 72
 	var response types.ServiceCreateResponse
46 73
 	resp, err := cli.post(ctx, "/services/create", nil, service, headers)
47 74
 	if err != nil {
... ...
@@ -58,6 +85,28 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
58 58
 	return response, err
59 59
 }
60 60
 
61
+func imageDigestAndPlatforms(ctx context.Context, cli *Client, image, encodedAuth string) (string, []swarm.Platform, error) {
62
+	distributionInspect, err := cli.DistributionInspect(ctx, image, encodedAuth)
63
+	imageWithDigest := image
64
+	var platforms []swarm.Platform
65
+	if err != nil {
66
+		return "", nil, err
67
+	}
68
+
69
+	imageWithDigest = imageWithDigestString(image, distributionInspect.Descriptor.Digest)
70
+
71
+	if len(distributionInspect.Platforms) > 0 {
72
+		platforms = make([]swarm.Platform, 0, len(distributionInspect.Platforms))
73
+		for _, p := range distributionInspect.Platforms {
74
+			platforms = append(platforms, swarm.Platform{
75
+				Architecture: p.Architecture,
76
+				OS:           p.OS,
77
+			})
78
+		}
79
+	}
80
+	return imageWithDigest, platforms, err
81
+}
82
+
61 83
 // imageWithDigestString takes an image string and a digest, and updates
62 84
 // the image string if it didn't originally contain a digest. It returns
63 85
 // an empty string if there are no updates.
... ...
@@ -86,27 +135,22 @@ func imageWithTagString(image string) string {
86 86
 	return ""
87 87
 }
88 88
 
89
-// setServicePlatforms sets Platforms in swarm.Placement to list all
90
-// compatible platforms for the service, as found in distributionInspect
91
-// and returns a pointer to the new or updated swarm.Placement struct.
92
-func setServicePlatforms(placement *swarm.Placement, distributionInspect registrytypes.DistributionInspect) *swarm.Placement {
93
-	if placement == nil {
94
-		placement = &swarm.Placement{}
95
-	}
96
-	// reset any existing listed platforms
97
-	placement.Platforms = []swarm.Platform{}
98
-	for _, p := range distributionInspect.Platforms {
99
-		placement.Platforms = append(placement.Platforms, swarm.Platform{
100
-			Architecture: p.Architecture,
101
-			OS:           p.OS,
102
-		})
103
-	}
104
-	return placement
105
-}
106
-
107 89
 // digestWarning constructs a formatted warning string using the
108 90
 // image name that could not be pinned by digest. The formatting
109 91
 // is hardcoded, but could me made smarter in the future
110 92
 func digestWarning(image string) string {
111 93
 	return fmt.Sprintf("image %s could not be accessed on a registry to record\nits digest. Each node will access %s independently,\npossibly leading to different nodes running different\nversions of the image.\n", image, image)
112 94
 }
95
+
96
+func validateServiceSpec(s swarm.ServiceSpec) error {
97
+	if s.TaskTemplate.ContainerSpec != nil && s.TaskTemplate.PluginSpec != nil {
98
+		return errors.New("must not specify both a container spec and a plugin spec in the task template")
99
+	}
100
+	if s.TaskTemplate.PluginSpec != nil && s.TaskTemplate.Runtime != swarm.RuntimePlugin {
101
+		return errors.New("mismatched runtime with plugin spec")
102
+	}
103
+	if s.TaskTemplate.ContainerSpec != nil && (s.TaskTemplate.Runtime != "" && s.TaskTemplate.Runtime != swarm.RuntimeContainer) {
104
+		return errors.New("mismatched runtime with container spec")
105
+	}
106
+	return nil
107
+}
... ...
@@ -112,7 +112,7 @@ func TestServiceCreateCompatiblePlatforms(t *testing.T) {
112 112
 		}),
113 113
 	}
114 114
 
115
-	spec := swarm.ServiceSpec{TaskTemplate: swarm.TaskSpec{ContainerSpec: swarm.ContainerSpec{Image: "foobar:1.0"}}}
115
+	spec := swarm.ServiceSpec{TaskTemplate: swarm.TaskSpec{ContainerSpec: &swarm.ContainerSpec{Image: "foobar:1.0"}}}
116 116
 
117 117
 	r, err := client.ServiceCreate(context.Background(), spec, types.ServiceCreateOptions{QueryRegistry: true})
118 118
 	assert.NoError(t, err)
... ...
@@ -189,7 +189,7 @@ func TestServiceCreateDigestPinning(t *testing.T) {
189 189
 	for _, p := range pinByDigestTests {
190 190
 		r, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{
191 191
 			TaskTemplate: swarm.TaskSpec{
192
-				ContainerSpec: swarm.ContainerSpec{
192
+				ContainerSpec: &swarm.ContainerSpec{
193 193
 					Image: p.img,
194 194
 				},
195 195
 			},
... ...
@@ -35,26 +35,46 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
35 35
 
36 36
 	query.Set("version", strconv.FormatUint(version.Index, 10))
37 37
 
38
-	// ensure that the image is tagged
39
-	if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
40
-		service.TaskTemplate.ContainerSpec.Image = taggedImg
38
+	if err := validateServiceSpec(service); err != nil {
39
+		return types.ServiceUpdateResponse{}, err
41 40
 	}
42 41
 
43
-	// Contact the registry to retrieve digest and platform information
44
-	// This happens only when the image has changed
45
-	if options.QueryRegistry {
46
-		distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth)
47
-		distErr = err
48
-		if err == nil {
49
-			// now pin by digest if the image doesn't already contain a digest
50
-			if img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest); img != "" {
42
+	var imgPlatforms []swarm.Platform
43
+	// ensure that the image is tagged
44
+	if service.TaskTemplate.ContainerSpec != nil {
45
+		if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
46
+			service.TaskTemplate.ContainerSpec.Image = taggedImg
47
+		}
48
+		if options.QueryRegistry {
49
+			var img string
50
+			img, imgPlatforms, distErr = imageDigestAndPlatforms(ctx, cli, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth)
51
+			if img != "" {
51 52
 				service.TaskTemplate.ContainerSpec.Image = img
52 53
 			}
53
-			// add platforms that are compatible with the service
54
-			service.TaskTemplate.Placement = setServicePlatforms(service.TaskTemplate.Placement, distributionInspect)
55 54
 		}
56 55
 	}
57 56
 
57
+	// ensure that the image is tagged
58
+	if service.TaskTemplate.PluginSpec != nil {
59
+		if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
60
+			service.TaskTemplate.PluginSpec.Remote = taggedImg
61
+		}
62
+		if options.QueryRegistry {
63
+			var img string
64
+			img, imgPlatforms, distErr = imageDigestAndPlatforms(ctx, cli, service.TaskTemplate.PluginSpec.Remote, options.EncodedRegistryAuth)
65
+			if img != "" {
66
+				service.TaskTemplate.PluginSpec.Remote = img
67
+			}
68
+		}
69
+	}
70
+
71
+	if service.TaskTemplate.Placement == nil && len(imgPlatforms) > 0 {
72
+		service.TaskTemplate.Placement = &swarm.Placement{}
73
+	}
74
+	if len(imgPlatforms) > 0 {
75
+		service.TaskTemplate.Placement.Platforms = imgPlatforms
76
+	}
77
+
58 78
 	var response types.ServiceUpdateResponse
59 79
 	resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
60 80
 	if err != nil {
... ...
@@ -253,6 +253,7 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
253 253
 		Root:                   cli.Config.Root,
254 254
 		Name:                   name,
255 255
 		Backend:                d,
256
+		PluginBackend:          d.PluginManager(),
256 257
 		NetworkSubnetsProvider: d,
257 258
 		DefaultAdvertiseAddr:   cli.Config.SwarmDefaultAdvertiseAddr,
258 259
 		RuntimeRoot:            cli.getSwarmRunRoot(),
... ...
@@ -49,6 +49,7 @@ import (
49 49
 	"github.com/Sirupsen/logrus"
50 50
 	"github.com/docker/docker/api/types/network"
51 51
 	types "github.com/docker/docker/api/types/swarm"
52
+	"github.com/docker/docker/daemon/cluster/controllers/plugin"
52 53
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
53 54
 	"github.com/docker/docker/pkg/signal"
54 55
 	lncluster "github.com/docker/libnetwork/cluster"
... ...
@@ -97,6 +98,7 @@ type Config struct {
97 97
 	Root                   string
98 98
 	Name                   string
99 99
 	Backend                executorpkg.Backend
100
+	PluginBackend          plugin.Backend
100 101
 	NetworkSubnetsProvider NetworkSubnetsProvider
101 102
 
102 103
 	// DefaultAdvertiseAddr is the default host/IP or network interface to use
... ...
@@ -1,79 +1,261 @@
1 1
 package plugin
2 2
 
3 3
 import (
4
+	"io"
5
+	"io/ioutil"
6
+	"net/http"
7
+
4 8
 	"github.com/Sirupsen/logrus"
9
+	"github.com/docker/distribution/reference"
10
+	enginetypes "github.com/docker/docker/api/types"
11
+	"github.com/docker/docker/api/types/swarm/runtime"
12
+	"github.com/docker/docker/plugin"
13
+	"github.com/docker/docker/plugin/v2"
5 14
 	"github.com/docker/swarmkit/api"
15
+	"github.com/gogo/protobuf/proto"
16
+	"github.com/pkg/errors"
6 17
 	"golang.org/x/net/context"
7 18
 )
8 19
 
9
-// Controller is the controller for the plugin backend
10
-type Controller struct{}
20
+// Controller is the controller for the plugin backend.
21
+// Plugins are managed as a singleton object with a desired state (different from containers).
22
+// With the the plugin controller instead of having a strict create->start->stop->remove
23
+// task lifecycle like containers, we manage the desired state of the plugin and let
24
+// the plugin manager do what it already does and monitor the plugin.
25
+// We'll also end up with many tasks all pointing to the same plugin ID.
26
+//
27
+// TODO(@cpuguy83): registry auth is intentionally not supported until we work out
28
+// the right way to pass registry crednetials via secrets.
29
+type Controller struct {
30
+	backend Backend
31
+	spec    runtime.PluginSpec
32
+	logger  *logrus.Entry
33
+
34
+	pluginID  string
35
+	serviceID string
36
+	taskID    string
37
+
38
+	// hook used to signal tests that `Wait()` is actually ready and waiting
39
+	signalWaitReady func()
40
+}
41
+
42
+// Backend is the interface for interacting with the plugin manager
43
+// Controller actions are passed to the configured backend to do the real work.
44
+type Backend interface {
45
+	Disable(name string, config *enginetypes.PluginDisableConfig) error
46
+	Enable(name string, config *enginetypes.PluginEnableConfig) error
47
+	Remove(name string, config *enginetypes.PluginRmConfig) error
48
+	Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer, opts ...plugin.CreateOpt) error
49
+	Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error
50
+	Get(name string) (*v2.Plugin, error)
51
+	SubscribeEvents(buffer int, events ...plugin.Event) (eventCh <-chan interface{}, cancel func())
52
+}
11 53
 
12 54
 // NewController returns a new cluster plugin controller
13
-func NewController() (*Controller, error) {
14
-	return &Controller{}, nil
55
+func NewController(backend Backend, t *api.Task) (*Controller, error) {
56
+	spec, err := readSpec(t)
57
+	if err != nil {
58
+		return nil, err
59
+	}
60
+	return &Controller{
61
+		backend:   backend,
62
+		spec:      spec,
63
+		serviceID: t.ServiceID,
64
+		logger: logrus.WithFields(logrus.Fields{
65
+			"controller": "plugin",
66
+			"task":       t.ID,
67
+			"plugin":     spec.Name,
68
+		})}, nil
69
+}
70
+
71
+func readSpec(t *api.Task) (runtime.PluginSpec, error) {
72
+	var cfg runtime.PluginSpec
73
+
74
+	generic := t.Spec.GetGeneric()
75
+	if err := proto.Unmarshal(generic.Payload.Value, &cfg); err != nil {
76
+		return cfg, errors.Wrap(err, "error reading plugin spec")
77
+	}
78
+	return cfg, nil
15 79
 }
16 80
 
17 81
 // Update is the update phase from swarmkit
18 82
 func (p *Controller) Update(ctx context.Context, t *api.Task) error {
19
-	logrus.WithFields(logrus.Fields{
20
-		"controller": "plugin",
21
-	}).Debug("Update")
83
+	p.logger.Debug("Update")
22 84
 	return nil
23 85
 }
24 86
 
25 87
 // Prepare is the prepare phase from swarmkit
26
-func (p *Controller) Prepare(ctx context.Context) error {
27
-	logrus.WithFields(logrus.Fields{
28
-		"controller": "plugin",
29
-	}).Debug("Prepare")
88
+func (p *Controller) Prepare(ctx context.Context) (err error) {
89
+	p.logger.Debug("Prepare")
90
+
91
+	remote, err := reference.ParseNormalizedNamed(p.spec.Remote)
92
+	if err != nil {
93
+		return errors.Wrapf(err, "error parsing remote reference %q", p.spec.Remote)
94
+	}
95
+
96
+	if p.spec.Name == "" {
97
+		p.spec.Name = remote.String()
98
+	}
99
+
100
+	var authConfig enginetypes.AuthConfig
101
+	privs := convertPrivileges(p.spec.Privileges)
102
+
103
+	pl, err := p.backend.Get(p.spec.Name)
104
+
105
+	defer func() {
106
+		if pl != nil && err == nil {
107
+			pl.Acquire()
108
+		}
109
+	}()
110
+
111
+	if err == nil && pl != nil {
112
+		if pl.SwarmServiceID != p.serviceID {
113
+			return errors.Errorf("plugin already exists: %s", p.spec.Name)
114
+		}
115
+		if pl.IsEnabled() {
116
+			if err := p.backend.Disable(pl.GetID(), &enginetypes.PluginDisableConfig{ForceDisable: true}); err != nil {
117
+				p.logger.WithError(err).Debug("could not disable plugin before running upgrade")
118
+			}
119
+		}
120
+		p.pluginID = pl.GetID()
121
+		return p.backend.Upgrade(ctx, remote, p.spec.Name, nil, &authConfig, privs, ioutil.Discard)
122
+	}
123
+
124
+	if err := p.backend.Pull(ctx, remote, p.spec.Name, nil, &authConfig, privs, ioutil.Discard, plugin.WithSwarmService(p.serviceID)); err != nil {
125
+		return err
126
+	}
127
+	pl, err = p.backend.Get(p.spec.Name)
128
+	if err != nil {
129
+		return err
130
+	}
131
+	p.pluginID = pl.GetID()
132
+
30 133
 	return nil
31 134
 }
32 135
 
33 136
 // Start is the start phase from swarmkit
34 137
 func (p *Controller) Start(ctx context.Context) error {
35
-	logrus.WithFields(logrus.Fields{
36
-		"controller": "plugin",
37
-	}).Debug("Start")
138
+	p.logger.Debug("Start")
139
+
140
+	pl, err := p.backend.Get(p.pluginID)
141
+	if err != nil {
142
+		return err
143
+	}
144
+
145
+	if p.spec.Disabled {
146
+		if pl.IsEnabled() {
147
+			return p.backend.Disable(p.pluginID, &enginetypes.PluginDisableConfig{ForceDisable: false})
148
+		}
149
+		return nil
150
+	}
151
+	if !pl.IsEnabled() {
152
+		return p.backend.Enable(p.pluginID, &enginetypes.PluginEnableConfig{Timeout: 30})
153
+	}
38 154
 	return nil
39 155
 }
40 156
 
41 157
 // Wait causes the task to wait until returned
42 158
 func (p *Controller) Wait(ctx context.Context) error {
43
-	logrus.WithFields(logrus.Fields{
44
-		"controller": "plugin",
45
-	}).Debug("Wait")
46
-	return nil
159
+	p.logger.Debug("Wait")
160
+
161
+	pl, err := p.backend.Get(p.pluginID)
162
+	if err != nil {
163
+		return err
164
+	}
165
+
166
+	events, cancel := p.backend.SubscribeEvents(1, plugin.EventDisable{Plugin: pl.PluginObj}, plugin.EventRemove{Plugin: pl.PluginObj}, plugin.EventEnable{Plugin: pl.PluginObj})
167
+	defer cancel()
168
+
169
+	if p.signalWaitReady != nil {
170
+		p.signalWaitReady()
171
+	}
172
+
173
+	if !p.spec.Disabled != pl.IsEnabled() {
174
+		return errors.New("mismatched plugin state")
175
+	}
176
+
177
+	for {
178
+		select {
179
+		case <-ctx.Done():
180
+			return ctx.Err()
181
+		case e := <-events:
182
+			p.logger.Debugf("got event %#T", e)
183
+
184
+			switch e.(type) {
185
+			case plugin.EventEnable:
186
+				if p.spec.Disabled {
187
+					return errors.New("plugin enabled")
188
+				}
189
+			case plugin.EventRemove:
190
+				return errors.New("plugin removed")
191
+			case plugin.EventDisable:
192
+				if !p.spec.Disabled {
193
+					return errors.New("plugin disabled")
194
+				}
195
+			}
196
+		}
197
+	}
198
+}
199
+
200
+func isNotFound(err error) bool {
201
+	_, ok := errors.Cause(err).(plugin.ErrNotFound)
202
+	return ok
47 203
 }
48 204
 
49 205
 // Shutdown is the shutdown phase from swarmkit
50 206
 func (p *Controller) Shutdown(ctx context.Context) error {
51
-	logrus.WithFields(logrus.Fields{
52
-		"controller": "plugin",
53
-	}).Debug("Shutdown")
207
+	p.logger.Debug("Shutdown")
54 208
 	return nil
55 209
 }
56 210
 
57 211
 // Terminate is the terminate phase from swarmkit
58 212
 func (p *Controller) Terminate(ctx context.Context) error {
59
-	logrus.WithFields(logrus.Fields{
60
-		"controller": "plugin",
61
-	}).Debug("Terminate")
213
+	p.logger.Debug("Terminate")
62 214
 	return nil
63 215
 }
64 216
 
65 217
 // Remove is the remove phase from swarmkit
66 218
 func (p *Controller) Remove(ctx context.Context) error {
67
-	logrus.WithFields(logrus.Fields{
68
-		"controller": "plugin",
69
-	}).Debug("Remove")
70
-	return nil
219
+	p.logger.Debug("Remove")
220
+
221
+	pl, err := p.backend.Get(p.pluginID)
222
+	if err != nil {
223
+		if isNotFound(err) {
224
+			return nil
225
+		}
226
+		return err
227
+	}
228
+
229
+	pl.Release()
230
+	if pl.GetRefCount() > 0 {
231
+		p.logger.Debug("skipping remove due to ref count")
232
+		return nil
233
+	}
234
+
235
+	// This may error because we have exactly 1 plugin, but potentially multiple
236
+	// tasks which are calling remove.
237
+	err = p.backend.Remove(p.pluginID, &enginetypes.PluginRmConfig{ForceRemove: true})
238
+	if isNotFound(err) {
239
+		return nil
240
+	}
241
+	return err
71 242
 }
72 243
 
73 244
 // Close is the close phase from swarmkit
74 245
 func (p *Controller) Close() error {
75
-	logrus.WithFields(logrus.Fields{
76
-		"controller": "plugin",
77
-	}).Debug("Close")
246
+	p.logger.Debug("Close")
78 247
 	return nil
79 248
 }
249
+
250
+func convertPrivileges(ls []*runtime.PluginPrivilege) enginetypes.PluginPrivileges {
251
+	var out enginetypes.PluginPrivileges
252
+	for _, p := range ls {
253
+		pp := enginetypes.PluginPrivilege{
254
+			Name:        p.Name,
255
+			Description: p.Description,
256
+			Value:       p.Value,
257
+		}
258
+		out = append(out, pp)
259
+	}
260
+	return out
261
+}
80 262
new file mode 100644
... ...
@@ -0,0 +1,390 @@
0
+package plugin
1
+
2
+import (
3
+	"errors"
4
+	"io"
5
+	"io/ioutil"
6
+	"net/http"
7
+	"strings"
8
+	"testing"
9
+	"time"
10
+
11
+	"github.com/Sirupsen/logrus"
12
+	"github.com/docker/distribution/reference"
13
+	enginetypes "github.com/docker/docker/api/types"
14
+	"github.com/docker/docker/api/types/swarm/runtime"
15
+	"github.com/docker/docker/pkg/pubsub"
16
+	"github.com/docker/docker/plugin"
17
+	"github.com/docker/docker/plugin/v2"
18
+	"golang.org/x/net/context"
19
+)
20
+
21
+const (
22
+	pluginTestName          = "test"
23
+	pluginTestRemote        = "testremote"
24
+	pluginTestRemoteUpgrade = "testremote2"
25
+)
26
+
27
+func TestPrepare(t *testing.T) {
28
+	b := newMockBackend()
29
+	c := newTestController(b, false)
30
+	ctx := context.Background()
31
+
32
+	if err := c.Prepare(ctx); err != nil {
33
+		t.Fatal(err)
34
+	}
35
+
36
+	if b.p == nil {
37
+		t.Fatal("pull not performed")
38
+	}
39
+
40
+	c = newTestController(b, false)
41
+	if err := c.Prepare(ctx); err != nil {
42
+		t.Fatal(err)
43
+	}
44
+	if b.p == nil {
45
+		t.Fatal("unexpected nil")
46
+	}
47
+	if b.p.PluginObj.PluginReference != pluginTestRemoteUpgrade {
48
+		t.Fatal("upgrade not performed")
49
+	}
50
+
51
+	c = newTestController(b, false)
52
+	c.serviceID = "1"
53
+	if err := c.Prepare(ctx); err == nil {
54
+		t.Fatal("expected error on prepare")
55
+	}
56
+}
57
+
58
+func TestStart(t *testing.T) {
59
+	b := newMockBackend()
60
+	c := newTestController(b, false)
61
+	ctx := context.Background()
62
+
63
+	if err := c.Prepare(ctx); err != nil {
64
+		t.Fatal(err)
65
+	}
66
+
67
+	if err := c.Start(ctx); err != nil {
68
+		t.Fatal(err)
69
+	}
70
+
71
+	if !b.p.IsEnabled() {
72
+		t.Fatal("expected plugin to be enabled")
73
+	}
74
+
75
+	c = newTestController(b, true)
76
+	if err := c.Prepare(ctx); err != nil {
77
+		t.Fatal(err)
78
+	}
79
+	if err := c.Start(ctx); err != nil {
80
+		t.Fatal(err)
81
+	}
82
+	if b.p.IsEnabled() {
83
+		t.Fatal("expected plugin to be disabled")
84
+	}
85
+
86
+	c = newTestController(b, false)
87
+	if err := c.Prepare(ctx); err != nil {
88
+		t.Fatal(err)
89
+	}
90
+	if err := c.Start(ctx); err != nil {
91
+		t.Fatal(err)
92
+	}
93
+	if !b.p.IsEnabled() {
94
+		t.Fatal("expected plugin to be enabled")
95
+	}
96
+}
97
+
98
+func TestWaitCancel(t *testing.T) {
99
+	b := newMockBackend()
100
+	c := newTestController(b, true)
101
+	ctx := context.Background()
102
+	if err := c.Prepare(ctx); err != nil {
103
+		t.Fatal(err)
104
+	}
105
+	if err := c.Start(ctx); err != nil {
106
+		t.Fatal(err)
107
+	}
108
+
109
+	ctxCancel, cancel := context.WithCancel(ctx)
110
+	chErr := make(chan error)
111
+	go func() {
112
+		chErr <- c.Wait(ctxCancel)
113
+	}()
114
+	cancel()
115
+	select {
116
+	case err := <-chErr:
117
+		if err != context.Canceled {
118
+			t.Fatal(err)
119
+		}
120
+	case <-time.After(10 * time.Second):
121
+		t.Fatal("timeout waiting for cancelation")
122
+	}
123
+}
124
+
125
+func TestWaitDisabled(t *testing.T) {
126
+	b := newMockBackend()
127
+	c := newTestController(b, true)
128
+	ctx := context.Background()
129
+	if err := c.Prepare(ctx); err != nil {
130
+		t.Fatal(err)
131
+	}
132
+	if err := c.Start(ctx); err != nil {
133
+		t.Fatal(err)
134
+	}
135
+
136
+	chErr := make(chan error)
137
+	go func() {
138
+		chErr <- c.Wait(ctx)
139
+	}()
140
+
141
+	if err := b.Enable("test", nil); err != nil {
142
+		t.Fatal(err)
143
+	}
144
+	select {
145
+	case err := <-chErr:
146
+		if err == nil {
147
+			t.Fatal("expected error")
148
+		}
149
+	case <-time.After(10 * time.Second):
150
+		t.Fatal("timeout waiting for event")
151
+	}
152
+
153
+	if err := c.Start(ctx); err != nil {
154
+		t.Fatal(err)
155
+	}
156
+
157
+	ctxWaitReady, cancelCtxWaitReady := context.WithTimeout(ctx, 30*time.Second)
158
+	c.signalWaitReady = cancelCtxWaitReady
159
+	defer cancelCtxWaitReady()
160
+
161
+	go func() {
162
+		chErr <- c.Wait(ctx)
163
+	}()
164
+
165
+	chEvent, cancel := b.SubscribeEvents(1)
166
+	defer cancel()
167
+
168
+	if err := b.Disable("test", nil); err != nil {
169
+		t.Fatal(err)
170
+	}
171
+
172
+	select {
173
+	case <-chEvent:
174
+		<-ctxWaitReady.Done()
175
+		if err := ctxWaitReady.Err(); err == context.DeadlineExceeded {
176
+			t.Fatal(err)
177
+		}
178
+		select {
179
+		case <-chErr:
180
+			t.Fatal("wait returned unexpectedly")
181
+		default:
182
+			// all good
183
+		}
184
+	case <-chErr:
185
+		t.Fatal("wait returned unexpectedly")
186
+	case <-time.After(10 * time.Second):
187
+		t.Fatal("timeout waiting for event")
188
+	}
189
+
190
+	if err := b.Remove("test", nil); err != nil {
191
+		t.Fatal(err)
192
+	}
193
+	select {
194
+	case err := <-chErr:
195
+		if err == nil {
196
+			t.Fatal("expected error")
197
+		}
198
+		if !strings.Contains(err.Error(), "removed") {
199
+			t.Fatal(err)
200
+		}
201
+	case <-time.After(10 * time.Second):
202
+		t.Fatal("timeout waiting for event")
203
+	}
204
+}
205
+
206
+func TestWaitEnabled(t *testing.T) {
207
+	b := newMockBackend()
208
+	c := newTestController(b, false)
209
+	ctx := context.Background()
210
+	if err := c.Prepare(ctx); err != nil {
211
+		t.Fatal(err)
212
+	}
213
+	if err := c.Start(ctx); err != nil {
214
+		t.Fatal(err)
215
+	}
216
+
217
+	chErr := make(chan error)
218
+	go func() {
219
+		chErr <- c.Wait(ctx)
220
+	}()
221
+
222
+	if err := b.Disable("test", nil); err != nil {
223
+		t.Fatal(err)
224
+	}
225
+	select {
226
+	case err := <-chErr:
227
+		if err == nil {
228
+			t.Fatal("expected error")
229
+		}
230
+	case <-time.After(10 * time.Second):
231
+		t.Fatal("timeout waiting for event")
232
+	}
233
+
234
+	if err := c.Start(ctx); err != nil {
235
+		t.Fatal(err)
236
+	}
237
+
238
+	ctxWaitReady, ctxWaitCancel := context.WithCancel(ctx)
239
+	c.signalWaitReady = ctxWaitCancel
240
+	defer ctxWaitCancel()
241
+
242
+	go func() {
243
+		chErr <- c.Wait(ctx)
244
+	}()
245
+
246
+	chEvent, cancel := b.SubscribeEvents(1)
247
+	defer cancel()
248
+
249
+	if err := b.Enable("test", nil); err != nil {
250
+		t.Fatal(err)
251
+	}
252
+
253
+	select {
254
+	case <-chEvent:
255
+		<-ctxWaitReady.Done()
256
+		if err := ctxWaitReady.Err(); err == context.DeadlineExceeded {
257
+			t.Fatal(err)
258
+		}
259
+		select {
260
+		case <-chErr:
261
+			t.Fatal("wait returned unexpectedly")
262
+		default:
263
+			// all good
264
+		}
265
+	case <-chErr:
266
+		t.Fatal("wait returned unexpectedly")
267
+	case <-time.After(10 * time.Second):
268
+		t.Fatal("timeout waiting for event")
269
+	}
270
+
271
+	if err := b.Remove("test", nil); err != nil {
272
+		t.Fatal(err)
273
+	}
274
+	select {
275
+	case err := <-chErr:
276
+		if err == nil {
277
+			t.Fatal("expected error")
278
+		}
279
+		if !strings.Contains(err.Error(), "removed") {
280
+			t.Fatal(err)
281
+		}
282
+	case <-time.After(10 * time.Second):
283
+		t.Fatal("timeout waiting for event")
284
+	}
285
+}
286
+
287
+func TestRemove(t *testing.T) {
288
+	b := newMockBackend()
289
+	c := newTestController(b, false)
290
+	ctx := context.Background()
291
+
292
+	if err := c.Prepare(ctx); err != nil {
293
+		t.Fatal(err)
294
+	}
295
+	if err := c.Shutdown(ctx); err != nil {
296
+		t.Fatal(err)
297
+	}
298
+
299
+	c2 := newTestController(b, false)
300
+	if err := c2.Prepare(ctx); err != nil {
301
+		t.Fatal(err)
302
+	}
303
+
304
+	if err := c.Remove(ctx); err != nil {
305
+		t.Fatal(err)
306
+	}
307
+	if b.p == nil {
308
+		t.Fatal("plugin removed unexpectedly")
309
+	}
310
+	if err := c2.Shutdown(ctx); err != nil {
311
+		t.Fatal(err)
312
+	}
313
+	if err := c2.Remove(ctx); err != nil {
314
+		t.Fatal(err)
315
+	}
316
+	if b.p != nil {
317
+		t.Fatal("expected plugin to be removed")
318
+	}
319
+}
320
+
321
+func newTestController(b Backend, disabled bool) *Controller {
322
+	return &Controller{
323
+		logger:  &logrus.Entry{Logger: &logrus.Logger{Out: ioutil.Discard}},
324
+		backend: b,
325
+		spec: runtime.PluginSpec{
326
+			Name:     pluginTestName,
327
+			Remote:   pluginTestRemote,
328
+			Disabled: disabled,
329
+		},
330
+	}
331
+}
332
+
333
+func newMockBackend() *mockBackend {
334
+	return &mockBackend{
335
+		pub: pubsub.NewPublisher(0, 0),
336
+	}
337
+}
338
+
339
+type mockBackend struct {
340
+	p   *v2.Plugin
341
+	pub *pubsub.Publisher
342
+}
343
+
344
+func (m *mockBackend) Disable(name string, config *enginetypes.PluginDisableConfig) error {
345
+	m.p.PluginObj.Enabled = false
346
+	m.pub.Publish(plugin.EventDisable{})
347
+	return nil
348
+}
349
+
350
+func (m *mockBackend) Enable(name string, config *enginetypes.PluginEnableConfig) error {
351
+	m.p.PluginObj.Enabled = true
352
+	m.pub.Publish(plugin.EventEnable{})
353
+	return nil
354
+}
355
+
356
+func (m *mockBackend) Remove(name string, config *enginetypes.PluginRmConfig) error {
357
+	m.p = nil
358
+	m.pub.Publish(plugin.EventRemove{})
359
+	return nil
360
+}
361
+
362
+func (m *mockBackend) Pull(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer, opts ...plugin.CreateOpt) error {
363
+	m.p = &v2.Plugin{
364
+		PluginObj: enginetypes.Plugin{
365
+			ID:              "1234",
366
+			Name:            name,
367
+			PluginReference: ref.String(),
368
+		},
369
+	}
370
+	return nil
371
+}
372
+
373
+func (m *mockBackend) Upgrade(ctx context.Context, ref reference.Named, name string, metaHeaders http.Header, authConfig *enginetypes.AuthConfig, privileges enginetypes.PluginPrivileges, outStream io.Writer) error {
374
+	m.p.PluginObj.PluginReference = pluginTestRemoteUpgrade
375
+	return nil
376
+}
377
+
378
+func (m *mockBackend) Get(name string) (*v2.Plugin, error) {
379
+	if m.p == nil {
380
+		return nil, errors.New("not found")
381
+	}
382
+	return m.p, nil
383
+}
384
+
385
+func (m *mockBackend) SubscribeEvents(buffer int, events ...plugin.Event) (eventCh <-chan interface{}, cancel func()) {
386
+	ch := m.pub.SubscribeTopicWithBuffer(nil, buffer)
387
+	cancel = func() { m.pub.Evict(ch) }
388
+	return ch, cancel
389
+}
... ...
@@ -13,8 +13,11 @@ import (
13 13
 	gogotypes "github.com/gogo/protobuf/types"
14 14
 )
15 15
 
16
-func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
17
-	containerSpec := types.ContainerSpec{
16
+func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
17
+	if c == nil {
18
+		return nil
19
+	}
20
+	containerSpec := &types.ContainerSpec{
18 21
 		Image:      c.Image,
19 22
 		Labels:     c.Labels,
20 23
 		Command:    c.Command,
... ...
@@ -211,7 +214,7 @@ func configReferencesFromGRPC(sr []*swarmapi.ConfigReference) []*types.ConfigRef
211 211
 	return refs
212 212
 }
213 213
 
214
-func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
214
+func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
215 215
 	containerSpec := &swarmapi.ContainerSpec{
216 216
 		Image:      c.Image,
217 217
 		Labels:     c.Labels,
... ...
@@ -1,14 +1,16 @@
1 1
 package convert
2 2
 
3 3
 import (
4
-	"errors"
5 4
 	"fmt"
6 5
 	"strings"
7 6
 
8 7
 	types "github.com/docker/docker/api/types/swarm"
8
+	"github.com/docker/docker/api/types/swarm/runtime"
9 9
 	"github.com/docker/docker/pkg/namesgenerator"
10 10
 	swarmapi "github.com/docker/swarmkit/api"
11
+	"github.com/gogo/protobuf/proto"
11 12
 	gogotypes "github.com/gogo/protobuf/types"
13
+	"github.com/pkg/errors"
12 14
 )
13 15
 
14 16
 var (
... ...
@@ -85,7 +87,10 @@ func serviceSpecFromGRPC(spec *swarmapi.ServiceSpec) (*types.ServiceSpec, error)
85 85
 
86 86
 	}
87 87
 
88
-	taskTemplate := taskSpecFromGRPC(spec.Task)
88
+	taskTemplate, err := taskSpecFromGRPC(spec.Task)
89
+	if err != nil {
90
+		return nil, err
91
+	}
89 92
 
90 93
 	switch t := spec.Task.GetRuntime().(type) {
91 94
 	case *swarmapi.TaskSpec_Container:
... ...
@@ -164,19 +169,34 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
164 164
 
165 165
 	switch s.TaskTemplate.Runtime {
166 166
 	case types.RuntimeContainer, "": // if empty runtime default to container
167
-		containerSpec, err := containerToGRPC(s.TaskTemplate.ContainerSpec)
168
-		if err != nil {
169
-			return swarmapi.ServiceSpec{}, err
167
+		if s.TaskTemplate.ContainerSpec != nil {
168
+			containerSpec, err := containerToGRPC(s.TaskTemplate.ContainerSpec)
169
+			if err != nil {
170
+				return swarmapi.ServiceSpec{}, err
171
+			}
172
+			spec.Task.Runtime = &swarmapi.TaskSpec_Container{Container: containerSpec}
170 173
 		}
171
-		spec.Task.Runtime = &swarmapi.TaskSpec_Container{Container: containerSpec}
172 174
 	case types.RuntimePlugin:
173
-		spec.Task.Runtime = &swarmapi.TaskSpec_Generic{
174
-			Generic: &swarmapi.GenericRuntimeSpec{
175
-				Kind: string(types.RuntimePlugin),
176
-				Payload: &gogotypes.Any{
177
-					TypeUrl: string(types.RuntimeURLPlugin),
175
+		if s.Mode.Replicated != nil {
176
+			return swarmapi.ServiceSpec{}, errors.New("plugins must not use replicated mode")
177
+		}
178
+
179
+		s.Mode.Global = &types.GlobalService{} // must always be global
180
+
181
+		if s.TaskTemplate.PluginSpec != nil {
182
+			pluginSpec, err := proto.Marshal(s.TaskTemplate.PluginSpec)
183
+			if err != nil {
184
+				return swarmapi.ServiceSpec{}, err
185
+			}
186
+			spec.Task.Runtime = &swarmapi.TaskSpec_Generic{
187
+				Generic: &swarmapi.GenericRuntimeSpec{
188
+					Kind: string(types.RuntimePlugin),
189
+					Payload: &gogotypes.Any{
190
+						TypeUrl: string(types.RuntimeURLPlugin),
191
+						Value:   pluginSpec,
192
+					},
178 193
 				},
179
-			},
194
+			}
180 195
 		}
181 196
 	default:
182 197
 		return swarmapi.ServiceSpec{}, ErrUnsupportedRuntime
... ...
@@ -507,21 +527,14 @@ func updateConfigToGRPC(updateConfig *types.UpdateConfig) (*swarmapi.UpdateConfi
507 507
 	return converted, nil
508 508
 }
509 509
 
510
-func taskSpecFromGRPC(taskSpec swarmapi.TaskSpec) types.TaskSpec {
510
+func taskSpecFromGRPC(taskSpec swarmapi.TaskSpec) (types.TaskSpec, error) {
511 511
 	taskNetworks := make([]types.NetworkAttachmentConfig, 0, len(taskSpec.Networks))
512 512
 	for _, n := range taskSpec.Networks {
513 513
 		netConfig := types.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverOpts: n.DriverAttachmentOpts}
514 514
 		taskNetworks = append(taskNetworks, netConfig)
515 515
 	}
516 516
 
517
-	c := taskSpec.GetContainer()
518
-	cSpec := types.ContainerSpec{}
519
-	if c != nil {
520
-		cSpec = containerSpecFromGRPC(c)
521
-	}
522
-
523
-	return types.TaskSpec{
524
-		ContainerSpec: cSpec,
517
+	t := types.TaskSpec{
525 518
 		Resources:     resourcesFromGRPC(taskSpec.Resources),
526 519
 		RestartPolicy: restartPolicyFromGRPC(taskSpec.Restart),
527 520
 		Placement:     placementFromGRPC(taskSpec.Placement),
... ...
@@ -529,4 +542,26 @@ func taskSpecFromGRPC(taskSpec swarmapi.TaskSpec) types.TaskSpec {
529 529
 		Networks:      taskNetworks,
530 530
 		ForceUpdate:   taskSpec.ForceUpdate,
531 531
 	}
532
+
533
+	switch taskSpec.GetRuntime().(type) {
534
+	case *swarmapi.TaskSpec_Container, nil:
535
+		c := taskSpec.GetContainer()
536
+		if c != nil {
537
+			t.ContainerSpec = containerSpecFromGRPC(c)
538
+		}
539
+	case *swarmapi.TaskSpec_Generic:
540
+		g := taskSpec.GetGeneric()
541
+		if g != nil {
542
+			switch g.Kind {
543
+			case string(types.RuntimePlugin):
544
+				var p runtime.PluginSpec
545
+				if err := proto.Unmarshal(g.Payload.Value, &p); err != nil {
546
+					return t, errors.Wrap(err, "error unmarshalling plugin spec")
547
+				}
548
+				t.PluginSpec = &p
549
+			}
550
+		}
551
+	}
552
+
553
+	return t, nil
532 554
 }
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"testing"
5 5
 
6 6
 	swarmtypes "github.com/docker/docker/api/types/swarm"
7
+	"github.com/docker/docker/api/types/swarm/runtime"
7 8
 	swarmapi "github.com/docker/swarmkit/api"
8 9
 	google_protobuf3 "github.com/gogo/protobuf/types"
9 10
 )
... ...
@@ -82,7 +83,8 @@ func TestServiceConvertFromGRPCGenericRuntimePlugin(t *testing.T) {
82 82
 func TestServiceConvertToGRPCGenericRuntimePlugin(t *testing.T) {
83 83
 	s := swarmtypes.ServiceSpec{
84 84
 		TaskTemplate: swarmtypes.TaskSpec{
85
-			Runtime: swarmtypes.RuntimePlugin,
85
+			Runtime:    swarmtypes.RuntimePlugin,
86
+			PluginSpec: &runtime.PluginSpec{},
86 87
 		},
87 88
 		Mode: swarmtypes.ServiceMode{
88 89
 			Global: &swarmtypes.GlobalService{},
... ...
@@ -108,7 +110,7 @@ func TestServiceConvertToGRPCContainerRuntime(t *testing.T) {
108 108
 	image := "alpine:latest"
109 109
 	s := swarmtypes.ServiceSpec{
110 110
 		TaskTemplate: swarmtypes.TaskSpec{
111
-			ContainerSpec: swarmtypes.ContainerSpec{
111
+			ContainerSpec: &swarmtypes.ContainerSpec{
112 112
 				Image: image,
113 113
 			},
114 114
 		},
... ...
@@ -9,19 +9,22 @@ import (
9 9
 )
10 10
 
11 11
 // TaskFromGRPC converts a grpc Task to a Task.
12
-func TaskFromGRPC(t swarmapi.Task) types.Task {
12
+func TaskFromGRPC(t swarmapi.Task) (types.Task, error) {
13 13
 	if t.Spec.GetAttachment() != nil {
14
-		return types.Task{}
14
+		return types.Task{}, nil
15 15
 	}
16 16
 	containerStatus := t.Status.GetContainer()
17
-
17
+	taskSpec, err := taskSpecFromGRPC(t.Spec)
18
+	if err != nil {
19
+		return types.Task{}, err
20
+	}
18 21
 	task := types.Task{
19 22
 		ID:          t.ID,
20 23
 		Annotations: annotationsFromGRPC(t.Annotations),
21 24
 		ServiceID:   t.ServiceID,
22 25
 		Slot:        int(t.Slot),
23 26
 		NodeID:      t.NodeID,
24
-		Spec:        taskSpecFromGRPC(t.Spec),
27
+		Spec:        taskSpec,
25 28
 		Status: types.TaskStatus{
26 29
 			State:   types.TaskState(strings.ToLower(t.Status.State.String())),
27 30
 			Message: t.Status.Message,
... ...
@@ -49,7 +52,7 @@ func TaskFromGRPC(t swarmapi.Task) types.Task {
49 49
 	}
50 50
 
51 51
 	if t.Status.PortStatus == nil {
52
-		return task
52
+		return task, nil
53 53
 	}
54 54
 
55 55
 	for _, p := range t.Status.PortStatus.Ports {
... ...
@@ -62,5 +65,5 @@ func TaskFromGRPC(t swarmapi.Task) types.Task {
62 62
 		})
63 63
 	}
64 64
 
65
-	return task
65
+	return task, nil
66 66
 }
... ...
@@ -22,15 +22,17 @@ import (
22 22
 )
23 23
 
24 24
 type executor struct {
25
-	backend      executorpkg.Backend
26
-	dependencies exec.DependencyManager
25
+	backend       executorpkg.Backend
26
+	pluginBackend plugin.Backend
27
+	dependencies  exec.DependencyManager
27 28
 }
28 29
 
29 30
 // NewExecutor returns an executor from the docker client.
30
-func NewExecutor(b executorpkg.Backend) exec.Executor {
31
+func NewExecutor(b executorpkg.Backend, p plugin.Backend) exec.Executor {
31 32
 	return &executor{
32
-		backend:      b,
33
-		dependencies: agent.NewDependencyManager(),
33
+		backend:       b,
34
+		pluginBackend: p,
35
+		dependencies:  agent.NewDependencyManager(),
34 36
 	}
35 37
 }
36 38
 
... ...
@@ -181,7 +183,7 @@ func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
181 181
 		}
182 182
 		switch runtimeKind {
183 183
 		case string(swarmtypes.RuntimePlugin):
184
-			c, err := plugin.NewController()
184
+			c, err := plugin.NewController(e.pluginBackend, t)
185 185
 			if err != nil {
186 186
 				return ctlr, err
187 187
 			}
... ...
@@ -57,6 +57,7 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e
57 57
 		// internal use in checking create/update progress. Therefore,
58 58
 		// we prefix it with a '_'.
59 59
 		"_up-to-date": true,
60
+		"runtime":     true,
60 61
 	}
61 62
 	if err := filter.Validate(accepted); err != nil {
62 63
 		return nil, err
... ...
@@ -73,6 +74,7 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e
73 73
 		ServiceIDs:   filter.Get("service"),
74 74
 		NodeIDs:      filter.Get("node"),
75 75
 		UpToDate:     len(filter.Get("_up-to-date")) != 0,
76
+		Runtimes:     filter.Get("runtime"),
76 77
 	}
77 78
 
78 79
 	for _, s := range filter.Get("desired-state") {
... ...
@@ -118,7 +118,7 @@ func (n *nodeRunner) start(conf nodeStartConfig) error {
118 118
 		JoinAddr:           joinAddr,
119 119
 		StateDir:           n.cluster.root,
120 120
 		JoinToken:          conf.joinToken,
121
-		Executor:           container.NewExecutor(n.cluster.config.Backend),
121
+		Executor:           container.NewExecutor(n.cluster.config.Backend, n.cluster.config.PluginBackend),
122 122
 		HeartbeatTick:      1,
123 123
 		ElectionTick:       3,
124 124
 		UnlockKey:          conf.lockKey,
... ...
@@ -50,14 +50,16 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
50 50
 		return nil, err
51 51
 	}
52 52
 
53
+	if len(options.Filters.Get("runtime")) == 0 {
54
+		// Default to using the container runtime filter
55
+		options.Filters.Add("runtime", string(types.RuntimeContainer))
56
+	}
57
+
53 58
 	filters := &swarmapi.ListServicesRequest_Filters{
54 59
 		NamePrefixes: options.Filters.Get("name"),
55 60
 		IDPrefixes:   options.Filters.Get("id"),
56 61
 		Labels:       runconfigopts.ConvertKVStringsToMap(options.Filters.Get("label")),
57
-		// (ehazlett): hardcode runtime for now. eventually we will
58
-		// be able to filter for the desired runtimes once more
59
-		// are supported.
60
-		Runtimes: []string{string(types.RuntimeContainer)},
62
+		Runtimes:     options.Filters.Get("runtime"),
61 63
 	}
62 64
 
63 65
 	ctx, cancel := c.getRequestContext()
... ...
@@ -134,6 +136,20 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string, queryRe
134 134
 
135 135
 		switch serviceSpec.Task.Runtime.(type) {
136 136
 		// handle other runtimes here
137
+		case *swarmapi.TaskSpec_Generic:
138
+			switch serviceSpec.Task.GetGeneric().Kind {
139
+			case string(types.RuntimePlugin):
140
+				if s.TaskTemplate.PluginSpec == nil {
141
+					return errors.New("plugin spec must be set")
142
+				}
143
+			}
144
+
145
+			r, err := state.controlClient.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
146
+			if err != nil {
147
+				return err
148
+			}
149
+
150
+			resp.ID = r.Service.ID
137 151
 		case *swarmapi.TaskSpec_Container:
138 152
 			ctnr := serviceSpec.Task.GetContainer()
139 153
 			if ctnr == nil {
... ...
@@ -146,7 +162,9 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string, queryRe
146 146
 			// retrieve auth config from encoded auth
147 147
 			authConfig := &apitypes.AuthConfig{}
148 148
 			if encodedAuth != "" {
149
-				if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
149
+				authReader := strings.NewReader(encodedAuth)
150
+				dec := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, authReader))
151
+				if err := dec.Decode(authConfig); err != nil {
150 152
 					logrus.Warnf("invalid authconfig: %v", err)
151 153
 				}
152 154
 			}
... ...
@@ -216,75 +234,85 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
216 216
 			return err
217 217
 		}
218 218
 
219
-		newCtnr := serviceSpec.Task.GetContainer()
220
-		if newCtnr == nil {
221
-			return errors.New("service does not use container tasks")
222
-		}
219
+		resp = &apitypes.ServiceUpdateResponse{}
223 220
 
224
-		encodedAuth := flags.EncodedRegistryAuth
225
-		if encodedAuth != "" {
226
-			newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
227
-		} else {
228
-			// this is needed because if the encodedAuth isn't being updated then we
229
-			// shouldn't lose it, and continue to use the one that was already present
230
-			var ctnr *swarmapi.ContainerSpec
231
-			switch flags.RegistryAuthFrom {
232
-			case apitypes.RegistryAuthFromSpec, "":
233
-				ctnr = currentService.Spec.Task.GetContainer()
234
-			case apitypes.RegistryAuthFromPreviousSpec:
235
-				if currentService.PreviousSpec == nil {
236
-					return errors.New("service does not have a previous spec")
221
+		switch serviceSpec.Task.Runtime.(type) {
222
+		case *swarmapi.TaskSpec_Generic:
223
+			switch serviceSpec.Task.GetGeneric().Kind {
224
+			case string(types.RuntimePlugin):
225
+				if spec.TaskTemplate.PluginSpec == nil {
226
+					return errors.New("plugin spec must be set")
237 227
 				}
238
-				ctnr = currentService.PreviousSpec.Task.GetContainer()
239
-			default:
240
-				return errors.New("unsupported registryAuthFrom value")
241 228
 			}
242
-			if ctnr == nil {
229
+		case *swarmapi.TaskSpec_Container:
230
+			newCtnr := serviceSpec.Task.GetContainer()
231
+			if newCtnr == nil {
243 232
 				return errors.New("service does not use container tasks")
244 233
 			}
245
-			newCtnr.PullOptions = ctnr.PullOptions
246
-			// update encodedAuth so it can be used to pin image by digest
247
-			if ctnr.PullOptions != nil {
248
-				encodedAuth = ctnr.PullOptions.RegistryAuth
234
+
235
+			encodedAuth := flags.EncodedRegistryAuth
236
+			if encodedAuth != "" {
237
+				newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
238
+			} else {
239
+				// this is needed because if the encodedAuth isn't being updated then we
240
+				// shouldn't lose it, and continue to use the one that was already present
241
+				var ctnr *swarmapi.ContainerSpec
242
+				switch flags.RegistryAuthFrom {
243
+				case apitypes.RegistryAuthFromSpec, "":
244
+					ctnr = currentService.Spec.Task.GetContainer()
245
+				case apitypes.RegistryAuthFromPreviousSpec:
246
+					if currentService.PreviousSpec == nil {
247
+						return errors.New("service does not have a previous spec")
248
+					}
249
+					ctnr = currentService.PreviousSpec.Task.GetContainer()
250
+				default:
251
+					return errors.New("unsupported registryAuthFrom value")
252
+				}
253
+				if ctnr == nil {
254
+					return errors.New("service does not use container tasks")
255
+				}
256
+				newCtnr.PullOptions = ctnr.PullOptions
257
+				// update encodedAuth so it can be used to pin image by digest
258
+				if ctnr.PullOptions != nil {
259
+					encodedAuth = ctnr.PullOptions.RegistryAuth
260
+				}
249 261
 			}
250
-		}
251 262
 
252
-		// retrieve auth config from encoded auth
253
-		authConfig := &apitypes.AuthConfig{}
254
-		if encodedAuth != "" {
255
-			if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
256
-				logrus.Warnf("invalid authconfig: %v", err)
263
+			// retrieve auth config from encoded auth
264
+			authConfig := &apitypes.AuthConfig{}
265
+			if encodedAuth != "" {
266
+				if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
267
+					logrus.Warnf("invalid authconfig: %v", err)
268
+				}
257 269
 			}
258
-		}
259 270
 
260
-		resp = &apitypes.ServiceUpdateResponse{}
271
+			// pin image by digest for API versions < 1.30
272
+			// TODO(nishanttotla): The check on "DOCKER_SERVICE_PREFER_OFFLINE_IMAGE"
273
+			// should be removed in the future. Since integration tests only use the
274
+			// latest API version, so this is no longer required.
275
+			if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" && queryRegistry {
276
+				digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig)
277
+				if err != nil {
278
+					logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())
279
+					// warning in the client response should be concise
280
+					resp.Warnings = append(resp.Warnings, digestWarning(newCtnr.Image))
281
+				} else if newCtnr.Image != digestImage {
282
+					logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
283
+					newCtnr.Image = digestImage
284
+				} else {
285
+					logrus.Debugf("updating service using supplied digest reference %s", newCtnr.Image)
286
+				}
261 287
 
262
-		// pin image by digest for API versions < 1.30
263
-		// TODO(nishanttotla): The check on "DOCKER_SERVICE_PREFER_OFFLINE_IMAGE"
264
-		// should be removed in the future. Since integration tests only use the
265
-		// latest API version, so this is no longer required.
266
-		if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" && queryRegistry {
267
-			digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig)
268
-			if err != nil {
269
-				logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())
270
-				// warning in the client response should be concise
271
-				resp.Warnings = append(resp.Warnings, digestWarning(newCtnr.Image))
272
-			} else if newCtnr.Image != digestImage {
273
-				logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
274
-				newCtnr.Image = digestImage
275
-			} else {
276
-				logrus.Debugf("updating service using supplied digest reference %s", newCtnr.Image)
288
+				// Replace the context with a fresh one.
289
+				// If we timed out while communicating with the
290
+				// registry, then "ctx" will already be expired, which
291
+				// would cause UpdateService below to fail. Reusing
292
+				// "ctx" could make it impossible to update a service
293
+				// if the registry is slow or unresponsive.
294
+				var cancel func()
295
+				ctx, cancel = c.getRequestContext()
296
+				defer cancel()
277 297
 			}
278
-
279
-			// Replace the context with a fresh one.
280
-			// If we timed out while communicating with the
281
-			// registry, then "ctx" will already be expired, which
282
-			// would cause UpdateService below to fail. Reusing
283
-			// "ctx" could make it impossible to update a service
284
-			// if the registry is slow or unresponsive.
285
-			var cancel func()
286
-			ctx, cancel = c.getRequestContext()
287
-			defer cancel()
288 298
 		}
289 299
 
290 300
 		var rollback swarmapi.UpdateServiceRequest_Rollback
... ...
@@ -19,7 +19,7 @@ func (c *Cluster) GetTasks(options apitypes.TaskListOptions) ([]types.Task, erro
19 19
 		return nil, c.errNoManager(state)
20 20
 	}
21 21
 
22
-	byName := func(filter filters.Args) error {
22
+	filterTransform := func(filter filters.Args) error {
23 23
 		if filter.Include("service") {
24 24
 			serviceFilters := filter.Get("service")
25 25
 			for _, serviceFilter := range serviceFilters {
... ...
@@ -42,10 +42,15 @@ func (c *Cluster) GetTasks(options apitypes.TaskListOptions) ([]types.Task, erro
42 42
 				filter.Add("node", node.ID)
43 43
 			}
44 44
 		}
45
+		if !filter.Include("runtime") {
46
+			// default to only showing container tasks
47
+			filter.Add("runtime", "container")
48
+			filter.Add("runtime", "")
49
+		}
45 50
 		return nil
46 51
 	}
47 52
 
48
-	filters, err := newListTasksFilters(options.Filters, byName)
53
+	filters, err := newListTasksFilters(options.Filters, filterTransform)
49 54
 	if err != nil {
50 55
 		return nil, err
51 56
 	}
... ...
@@ -61,11 +66,12 @@ func (c *Cluster) GetTasks(options apitypes.TaskListOptions) ([]types.Task, erro
61 61
 	}
62 62
 
63 63
 	tasks := make([]types.Task, 0, len(r.Tasks))
64
-
65 64
 	for _, task := range r.Tasks {
66
-		if task.Spec.GetContainer() != nil {
67
-			tasks = append(tasks, convert.TaskFromGRPC(*task))
65
+		t, err := convert.TaskFromGRPC(*task)
66
+		if err != nil {
67
+			return nil, err
68 68
 		}
69
+		tasks = append(tasks, t)
69 70
 	}
70 71
 	return tasks, nil
71 72
 }
... ...
@@ -83,5 +89,5 @@ func (c *Cluster) GetTask(input string) (types.Task, error) {
83 83
 	}); err != nil {
84 84
 		return types.Task{}, err
85 85
 	}
86
-	return convert.TaskFromGRPC(*task), nil
86
+	return convert.TaskFromGRPC(*task)
87 87
 }
... ...
@@ -28,6 +28,7 @@ keywords: "API, Docker, rcli, REST, documentation"
28 28
 * `GET /images/(name)/get` now includes an `ImageMetadata` field which contains image metadata that is local to the engine and not part of the image config.
29 29
 * `POST /swarm/init` now accepts a `DataPathAddr` property to set the IP-address or network interface to use for data traffic
30 30
 * `POST /swarm/join` now accepts a `DataPathAddr` property to set the IP-address or network interface to use for data traffic
31
+* `POST /services/create` now accepts a `PluginSpec` when `TaskTemplate.Runtime` is set to `plugin`
31 32
 
32 33
 ## v1.30 API changes
33 34
 
... ...
@@ -1,6 +1,7 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
+	"context"
4 5
 	"encoding/json"
5 6
 	"fmt"
6 7
 	"net/http"
... ...
@@ -10,6 +11,7 @@ import (
10 10
 	"github.com/docker/docker/api/types"
11 11
 	"github.com/docker/docker/api/types/filters"
12 12
 	"github.com/docker/docker/api/types/swarm"
13
+	"github.com/docker/docker/client"
13 14
 	"github.com/docker/docker/integration-cli/checker"
14 15
 	"github.com/go-check/check"
15 16
 	"github.com/pkg/errors"
... ...
@@ -124,20 +126,29 @@ type ConfigConstructor func(*swarm.Config)
124 124
 // SpecConstructor defines a swarm spec constructor
125 125
 type SpecConstructor func(*swarm.Spec)
126 126
 
127
-// CreateService creates a swarm service given the specified service constructor
128
-func (d *Swarm) CreateService(c *check.C, f ...ServiceConstructor) string {
127
+// CreateServiceWithOptions creates a swarm service given the specified service constructors
128
+// and auth config
129
+func (d *Swarm) CreateServiceWithOptions(c *check.C, opts types.ServiceCreateOptions, f ...ServiceConstructor) string {
130
+	cl, err := client.NewClient(d.Sock(), "", nil, nil)
131
+	c.Assert(err, checker.IsNil, check.Commentf("failed to create client"))
132
+	defer cl.Close()
133
+
129 134
 	var service swarm.Service
130 135
 	for _, fn := range f {
131 136
 		fn(&service)
132 137
 	}
133
-	status, out, err := d.SockRequest("POST", "/services/create", service.Spec)
134 138
 
135
-	c.Assert(err, checker.IsNil, check.Commentf(string(out)))
136
-	c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf("output: %q", string(out)))
139
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
140
+	defer cancel()
137 141
 
138
-	var scr types.ServiceCreateResponse
139
-	c.Assert(json.Unmarshal(out, &scr), checker.IsNil)
140
-	return scr.ID
142
+	res, err := cl.ServiceCreate(ctx, service.Spec, opts)
143
+	c.Assert(err, checker.IsNil)
144
+	return res.ID
145
+}
146
+
147
+// CreateService creates a swarm service given the specified service constructor
148
+func (d *Swarm) CreateService(c *check.C, f ...ServiceConstructor) string {
149
+	return d.CreateServiceWithOptions(c, types.ServiceCreateOptions{}, f...)
141 150
 }
142 151
 
143 152
 // GetService returns the swarm service corresponding to the specified id
... ...
@@ -200,6 +211,37 @@ func (d *Swarm) CheckServiceUpdateState(service string) func(*check.C) (interfac
200 200
 	}
201 201
 }
202 202
 
203
+// CheckPluginRunning returns the runtime state of the plugin
204
+func (d *Swarm) CheckPluginRunning(plugin string) func(c *check.C) (interface{}, check.CommentInterface) {
205
+	return func(c *check.C) (interface{}, check.CommentInterface) {
206
+		status, out, err := d.SockRequest("GET", "/plugins/"+plugin+"/json", nil)
207
+		c.Assert(err, checker.IsNil, check.Commentf(string(out)))
208
+		if status != http.StatusOK {
209
+			return false, nil
210
+		}
211
+
212
+		var p types.Plugin
213
+		c.Assert(json.Unmarshal(out, &p), checker.IsNil, check.Commentf(string(out)))
214
+
215
+		return p.Enabled, check.Commentf("%+v", p)
216
+	}
217
+}
218
+
219
+// CheckPluginImage returns the runtime state of the plugin
220
+func (d *Swarm) CheckPluginImage(plugin string) func(c *check.C) (interface{}, check.CommentInterface) {
221
+	return func(c *check.C) (interface{}, check.CommentInterface) {
222
+		status, out, err := d.SockRequest("GET", "/plugins/"+plugin+"/json", nil)
223
+		c.Assert(err, checker.IsNil, check.Commentf(string(out)))
224
+		if status != http.StatusOK {
225
+			return false, nil
226
+		}
227
+
228
+		var p types.Plugin
229
+		c.Assert(json.Unmarshal(out, &p), checker.IsNil, check.Commentf(string(out)))
230
+		return p.PluginReference, check.Commentf("%+v", p)
231
+	}
232
+}
233
+
203 234
 // CheckServiceTasks returns the number of tasks for the specified service
204 235
 func (d *Swarm) CheckServiceTasks(service string) func(*check.C) (interface{}, check.CommentInterface) {
205 236
 	return func(c *check.C) (interface{}, check.CommentInterface) {
... ...
@@ -247,7 +289,7 @@ func (d *Swarm) CheckRunningTaskImages(c *check.C) (interface{}, check.CommentIn
247 247
 
248 248
 	result := make(map[string]int)
249 249
 	for _, task := range tasks {
250
-		if task.Status.State == swarm.TaskStateRunning {
250
+		if task.Status.State == swarm.TaskStateRunning && task.Spec.ContainerSpec != nil {
251 251
 			result[task.Spec.ContainerSpec.Image]++
252 252
 		}
253 253
 	}
... ...
@@ -4,15 +4,19 @@ package main
4 4
 
5 5
 import (
6 6
 	"fmt"
7
+	"path"
7 8
 	"strconv"
8 9
 	"strings"
9 10
 	"syscall"
10 11
 	"time"
11 12
 
12 13
 	"github.com/docker/docker/api/types/swarm"
14
+	"github.com/docker/docker/api/types/swarm/runtime"
13 15
 	"github.com/docker/docker/integration-cli/checker"
14 16
 	"github.com/docker/docker/integration-cli/daemon"
17
+	"github.com/docker/docker/integration-cli/fixtures/plugin"
15 18
 	"github.com/go-check/check"
19
+	"golang.org/x/net/context"
16 20
 )
17 21
 
18 22
 func setPortConfig(portConfig []swarm.PortConfig) daemon.ServiceConstructor {
... ...
@@ -596,3 +600,77 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesStateReporting(c *check.C) {
596 596
 		}
597 597
 	}
598 598
 }
599
+
600
+// Test plugins deployed via swarm services
601
+func (s *DockerSwarmSuite) TestAPISwarmServicesPlugin(c *check.C) {
602
+	testRequires(c, DaemonIsLinux, IsAmd64)
603
+	reg := setupRegistry(c, false, "", "")
604
+	defer reg.Close()
605
+
606
+	repo := path.Join(privateRegistryURL, "swarm", "test:v1")
607
+	repo2 := path.Join(privateRegistryURL, "swarm", "test:v2")
608
+	name := "test"
609
+
610
+	err := plugin.CreateInRegistry(context.Background(), repo, nil)
611
+	c.Assert(err, checker.IsNil, check.Commentf("failed to create plugin"))
612
+	err = plugin.CreateInRegistry(context.Background(), repo2, nil)
613
+	c.Assert(err, checker.IsNil, check.Commentf("failed to create plugin"))
614
+
615
+	d1 := s.AddDaemon(c, true, true)
616
+	d2 := s.AddDaemon(c, true, true)
617
+	d3 := s.AddDaemon(c, true, false)
618
+
619
+	makePlugin := func(repo, name string, constraints []string) func(*swarm.Service) {
620
+		return func(s *swarm.Service) {
621
+			s.Spec.TaskTemplate.Runtime = "plugin"
622
+			s.Spec.TaskTemplate.PluginSpec = &runtime.PluginSpec{
623
+				Name:   name,
624
+				Remote: repo,
625
+			}
626
+			if constraints != nil {
627
+				s.Spec.TaskTemplate.Placement = &swarm.Placement{
628
+					Constraints: constraints,
629
+				}
630
+			}
631
+		}
632
+	}
633
+
634
+	id := d1.CreateService(c, makePlugin(repo, name, nil))
635
+	waitAndAssert(c, defaultReconciliationTimeout, d1.CheckPluginRunning(name), checker.True)
636
+	waitAndAssert(c, defaultReconciliationTimeout, d2.CheckPluginRunning(name), checker.True)
637
+	waitAndAssert(c, defaultReconciliationTimeout, d3.CheckPluginRunning(name), checker.True)
638
+
639
+	service := d1.GetService(c, id)
640
+	d1.UpdateService(c, service, makePlugin(repo2, name, nil))
641
+	waitAndAssert(c, defaultReconciliationTimeout, d1.CheckPluginImage(name), checker.Equals, repo2)
642
+	waitAndAssert(c, defaultReconciliationTimeout, d2.CheckPluginImage(name), checker.Equals, repo2)
643
+	waitAndAssert(c, defaultReconciliationTimeout, d3.CheckPluginImage(name), checker.Equals, repo2)
644
+	waitAndAssert(c, defaultReconciliationTimeout, d1.CheckPluginRunning(name), checker.True)
645
+	waitAndAssert(c, defaultReconciliationTimeout, d2.CheckPluginRunning(name), checker.True)
646
+	waitAndAssert(c, defaultReconciliationTimeout, d3.CheckPluginRunning(name), checker.True)
647
+
648
+	d1.RemoveService(c, id)
649
+	waitAndAssert(c, defaultReconciliationTimeout, d1.CheckPluginRunning(name), checker.False)
650
+	waitAndAssert(c, defaultReconciliationTimeout, d2.CheckPluginRunning(name), checker.False)
651
+	waitAndAssert(c, defaultReconciliationTimeout, d3.CheckPluginRunning(name), checker.False)
652
+
653
+	// constrain to managers only
654
+	id = d1.CreateService(c, makePlugin(repo, name, []string{"node.role==manager"}))
655
+	waitAndAssert(c, defaultReconciliationTimeout, d1.CheckPluginRunning(name), checker.True)
656
+	waitAndAssert(c, defaultReconciliationTimeout, d2.CheckPluginRunning(name), checker.True)
657
+	waitAndAssert(c, defaultReconciliationTimeout, d3.CheckPluginRunning(name), checker.False) // Not a manager, not running it
658
+	d1.RemoveService(c, id)
659
+	waitAndAssert(c, defaultReconciliationTimeout, d1.CheckPluginRunning(name), checker.False)
660
+	waitAndAssert(c, defaultReconciliationTimeout, d2.CheckPluginRunning(name), checker.False)
661
+	waitAndAssert(c, defaultReconciliationTimeout, d3.CheckPluginRunning(name), checker.False)
662
+
663
+	// with no name
664
+	id = d1.CreateService(c, makePlugin(repo, "", nil))
665
+	waitAndAssert(c, defaultReconciliationTimeout, d1.CheckPluginRunning(repo), checker.True)
666
+	waitAndAssert(c, defaultReconciliationTimeout, d2.CheckPluginRunning(repo), checker.True)
667
+	waitAndAssert(c, defaultReconciliationTimeout, d3.CheckPluginRunning(repo), checker.True)
668
+	d1.RemoveService(c, id)
669
+	waitAndAssert(c, defaultReconciliationTimeout, d1.CheckPluginRunning(repo), checker.False)
670
+	waitAndAssert(c, defaultReconciliationTimeout, d2.CheckPluginRunning(repo), checker.False)
671
+	waitAndAssert(c, defaultReconciliationTimeout, d3.CheckPluginRunning(repo), checker.False)
672
+}
... ...
@@ -560,7 +560,7 @@ func simpleTestService(s *swarm.Service) {
560 560
 
561 561
 	s.Spec = swarm.ServiceSpec{
562 562
 		TaskTemplate: swarm.TaskSpec{
563
-			ContainerSpec: swarm.ContainerSpec{
563
+			ContainerSpec: &swarm.ContainerSpec{
564 564
 				Image:   "busybox:latest",
565 565
 				Command: []string{"/bin/top"},
566 566
 			},
... ...
@@ -583,7 +583,7 @@ func serviceForUpdate(s *swarm.Service) {
583 583
 
584 584
 	s.Spec = swarm.ServiceSpec{
585 585
 		TaskTemplate: swarm.TaskSpec{
586
-			ContainerSpec: swarm.ContainerSpec{
586
+			ContainerSpec: &swarm.ContainerSpec{
587 587
 				Image:   "busybox:latest",
588 588
 				Command: []string{"/bin/top"},
589 589
 			},
... ...
@@ -641,6 +641,9 @@ func setRollbackOrder(order string) daemon.ServiceConstructor {
641 641
 
642 642
 func setImage(image string) daemon.ServiceConstructor {
643 643
 	return func(s *swarm.Service) {
644
+		if s.Spec.TaskTemplate.ContainerSpec == nil {
645
+			s.Spec.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
646
+		}
644 647
 		s.Spec.TaskTemplate.ContainerSpec.Image = image
645 648
 	}
646 649
 }
... ...
@@ -921,6 +924,9 @@ func (s *DockerSwarmSuite) TestAPISwarmHealthcheckNone(c *check.C) {
921 921
 
922 922
 	instances := 1
923 923
 	d.CreateService(c, simpleTestService, setInstances(instances), func(s *swarm.Service) {
924
+		if s.Spec.TaskTemplate.ContainerSpec == nil {
925
+			s.Spec.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
926
+		}
924 927
 		s.Spec.TaskTemplate.ContainerSpec.Healthcheck = &container.HealthConfig{}
925 928
 		s.Spec.TaskTemplate.Networks = []swarm.NetworkAttachmentConfig{
926 929
 			{Target: "lb"},
927 930
new file mode 100644
... ...
@@ -0,0 +1,34 @@
0
+package main
1
+
2
+import (
3
+	"fmt"
4
+	"net"
5
+	"net/http"
6
+	"os"
7
+	"path/filepath"
8
+)
9
+
10
+func main() {
11
+	p, err := filepath.Abs(filepath.Join("run", "docker", "plugins"))
12
+	if err != nil {
13
+		panic(err)
14
+	}
15
+	if err := os.MkdirAll(p, 0755); err != nil {
16
+		panic(err)
17
+	}
18
+	l, err := net.Listen("unix", filepath.Join(p, "basic.sock"))
19
+	if err != nil {
20
+		panic(err)
21
+	}
22
+
23
+	mux := http.NewServeMux()
24
+	server := http.Server{
25
+		Addr:    l.Addr().String(),
26
+		Handler: http.NewServeMux(),
27
+	}
28
+	mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
29
+		w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1.1+json")
30
+		fmt.Println(w, `{"Implements": ["dummy"]}`)
31
+	})
32
+	server.Serve(l)
33
+}
0 34
new file mode 100644
... ...
@@ -0,0 +1,183 @@
0
+package plugin
1
+
2
+import (
3
+	"encoding/json"
4
+	"io"
5
+	"io/ioutil"
6
+	"os"
7
+	"os/exec"
8
+	"path/filepath"
9
+	"time"
10
+
11
+	"github.com/docker/docker/api/types"
12
+	"github.com/docker/docker/libcontainerd"
13
+	"github.com/docker/docker/pkg/archive"
14
+	"github.com/docker/docker/plugin"
15
+	"github.com/docker/docker/registry"
16
+	"github.com/pkg/errors"
17
+	"golang.org/x/net/context"
18
+)
19
+
20
+// CreateOpt is is passed used to change the defualt plugin config before
21
+// creating it
22
+type CreateOpt func(*Config)
23
+
24
+// Config wraps types.PluginConfig to provide some extra state for options
25
+// extra customizations on the plugin details, such as using a custom binary to
26
+// create the plugin with.
27
+type Config struct {
28
+	*types.PluginConfig
29
+	binPath string
30
+}
31
+
32
+// WithBinary is a CreateOpt to set an custom binary to create the plugin with.
33
+// This binary must be statically compiled.
34
+func WithBinary(bin string) CreateOpt {
35
+	return func(cfg *Config) {
36
+		cfg.binPath = bin
37
+	}
38
+}
39
+
40
+// CreateClient is the interface used for `BuildPlugin` to interact with the
41
+// daemon.
42
+type CreateClient interface {
43
+	PluginCreate(context.Context, io.Reader, types.PluginCreateOptions) error
44
+}
45
+
46
+// Create creates a new plugin with the specified name
47
+func Create(ctx context.Context, c CreateClient, name string, opts ...CreateOpt) error {
48
+	tmpDir, err := ioutil.TempDir("", "create-test-plugin")
49
+	if err != nil {
50
+		return err
51
+	}
52
+	defer os.RemoveAll(tmpDir)
53
+
54
+	tar, err := makePluginBundle(tmpDir, opts...)
55
+	if err != nil {
56
+		return err
57
+	}
58
+	defer tar.Close()
59
+
60
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
61
+	defer cancel()
62
+
63
+	return c.PluginCreate(ctx, tar, types.PluginCreateOptions{RepoName: name})
64
+}
65
+
66
+// TODO(@cpuguy83): we really shouldn't have to do this...
67
+// The manager panics on init when `Executor` is not set.
68
+type dummyExecutor struct{}
69
+
70
+func (dummyExecutor) Client(libcontainerd.Backend) (libcontainerd.Client, error) { return nil, nil }
71
+func (dummyExecutor) Cleanup()                                                   {}
72
+func (dummyExecutor) UpdateOptions(...libcontainerd.RemoteOption) error          { return nil }
73
+
74
+// CreateInRegistry makes a plugin (locally) and pushes it to a registry.
75
+// This does not use a dockerd instance to create or push the plugin.
76
+// If you just want to create a plugin in some daemon, use `Create`.
77
+//
78
+// This can be useful when testing plugins on swarm where you don't really want
79
+// the plugin to exist on any of the daemons (immediately) and there needs to be
80
+// some way to distribute the plugin.
81
+func CreateInRegistry(ctx context.Context, repo string, auth *types.AuthConfig, opts ...CreateOpt) error {
82
+	tmpDir, err := ioutil.TempDir("", "create-test-plugin-local")
83
+	if err != nil {
84
+		return err
85
+	}
86
+	defer os.RemoveAll(tmpDir)
87
+
88
+	inPath := filepath.Join(tmpDir, "plugin")
89
+	if err := os.MkdirAll(inPath, 0755); err != nil {
90
+		return errors.Wrap(err, "error creating plugin root")
91
+	}
92
+
93
+	tar, err := makePluginBundle(inPath, opts...)
94
+	if err != nil {
95
+		return err
96
+	}
97
+	defer tar.Close()
98
+
99
+	managerConfig := plugin.ManagerConfig{
100
+		Store:           plugin.NewStore(),
101
+		RegistryService: registry.NewService(registry.ServiceOptions{V2Only: true}),
102
+		Root:            filepath.Join(tmpDir, "root"),
103
+		ExecRoot:        "/run/docker", // manager init fails if not set
104
+		Executor:        dummyExecutor{},
105
+		LogPluginEvent:  func(id, name, action string) {}, // panics when not set
106
+	}
107
+	manager, err := plugin.NewManager(managerConfig)
108
+	if err != nil {
109
+		return errors.Wrap(err, "error creating plugin manager")
110
+	}
111
+
112
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
113
+	defer cancel()
114
+	if err := manager.CreateFromContext(ctx, tar, &types.PluginCreateOptions{RepoName: repo}); err != nil {
115
+		return err
116
+	}
117
+
118
+	if auth == nil {
119
+		auth = &types.AuthConfig{}
120
+	}
121
+	err = manager.Push(ctx, repo, nil, auth, ioutil.Discard)
122
+	return errors.Wrap(err, "error pushing plugin")
123
+}
124
+
125
+func makePluginBundle(inPath string, opts ...CreateOpt) (io.ReadCloser, error) {
126
+	p := &types.PluginConfig{
127
+		Interface: types.PluginConfigInterface{
128
+			Socket: "basic.sock",
129
+			Types:  []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}},
130
+		},
131
+		Entrypoint: []string{"/basic"},
132
+	}
133
+	cfg := &Config{
134
+		PluginConfig: p,
135
+	}
136
+	for _, o := range opts {
137
+		o(cfg)
138
+	}
139
+	if cfg.binPath == "" {
140
+		binPath, err := ensureBasicPluginBin()
141
+		if err != nil {
142
+			return nil, err
143
+		}
144
+		cfg.binPath = binPath
145
+	}
146
+
147
+	configJSON, err := json.Marshal(p)
148
+	if err != nil {
149
+		return nil, err
150
+	}
151
+	if err := ioutil.WriteFile(filepath.Join(inPath, "config.json"), configJSON, 0644); err != nil {
152
+		return nil, err
153
+	}
154
+	if err := os.MkdirAll(filepath.Join(inPath, "rootfs", filepath.Dir(p.Entrypoint[0])), 0755); err != nil {
155
+		return nil, errors.Wrap(err, "error creating plugin rootfs dir")
156
+	}
157
+	if err := archive.NewDefaultArchiver().CopyFileWithTar(cfg.binPath, filepath.Join(inPath, "rootfs", p.Entrypoint[0])); err != nil {
158
+		return nil, errors.Wrap(err, "error copying plugin binary to rootfs path")
159
+	}
160
+	tar, err := archive.Tar(inPath, archive.Uncompressed)
161
+	return tar, errors.Wrap(err, "error making plugin archive")
162
+}
163
+
164
+func ensureBasicPluginBin() (string, error) {
165
+	name := "docker-basic-plugin"
166
+	p, err := exec.LookPath(name)
167
+	if err == nil {
168
+		return p, nil
169
+	}
170
+
171
+	goBin, err := exec.LookPath("go")
172
+	if err != nil {
173
+		return "", err
174
+	}
175
+	installPath := filepath.Join(os.Getenv("GOPATH"), "bin", name)
176
+	cmd := exec.Command(goBin, "build", "-o", installPath, "./"+filepath.Join("fixtures", "plugin", "basic"))
177
+	cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
178
+	if out, err := cmd.CombinedOutput(); err != nil {
179
+		return "", errors.Wrapf(err, "error building basic plugin bin: %s", string(out))
180
+	}
181
+	return installPath, nil
182
+}
... ...
@@ -53,6 +53,16 @@ func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
53 53
 	return ch
54 54
 }
55 55
 
56
+// SubscribeTopicWithBuffer adds a new subscriber that filters messages sent by a topic.
57
+// The returned channel has a buffer of the specified size.
58
+func (p *Publisher) SubscribeTopicWithBuffer(topic topicFunc, buffer int) chan interface{} {
59
+	ch := make(chan interface{}, buffer)
60
+	p.m.Lock()
61
+	p.subscribers[ch] = topic
62
+	p.m.Unlock()
63
+	return ch
64
+}
65
+
56 66
 // Evict removes the specified subscriber from receiving any more messages.
57 67
 func (p *Publisher) Evict(sub chan interface{}) {
58 68
 	p.m.Lock()
... ...
@@ -67,6 +67,7 @@ func (pm *Manager) Disable(refOrID string, config *types.PluginDisableConfig) er
67 67
 	if err := pm.disable(p, c); err != nil {
68 68
 		return err
69 69
 	}
70
+	pm.publisher.Publish(EventDisable{Plugin: p.PluginObj})
70 71
 	pm.config.LogPluginEvent(p.GetID(), refOrID, "disable")
71 72
 	return nil
72 73
 }
... ...
@@ -82,6 +83,7 @@ func (pm *Manager) Enable(refOrID string, config *types.PluginEnableConfig) erro
82 82
 	if err := pm.enable(p, c, false); err != nil {
83 83
 		return err
84 84
 	}
85
+	pm.publisher.Publish(EventEnable{Plugin: p.PluginObj})
85 86
 	pm.config.LogPluginEvent(p.GetID(), refOrID, "enable")
86 87
 	return nil
87 88
 }
... ...
@@ -296,7 +298,7 @@ func (pm *Manager) Upgrade(ctx context.Context, ref reference.Named, name string
296 296
 }
297 297
 
298 298
 // Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
299
-func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer) (err error) {
299
+func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, outStream io.Writer, opts ...CreateOpt) (err error) {
300 300
 	pm.muGC.RLock()
301 301
 	defer pm.muGC.RUnlock()
302 302
 
... ...
@@ -340,12 +342,19 @@ func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, m
340 340
 		return err
341 341
 	}
342 342
 
343
-	p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges)
343
+	refOpt := func(p *v2.Plugin) {
344
+		p.PluginObj.PluginReference = ref.String()
345
+	}
346
+	optsList := make([]CreateOpt, 0, len(opts)+1)
347
+	optsList = append(optsList, opts...)
348
+	optsList = append(optsList, refOpt)
349
+
350
+	p, err := pm.createPlugin(name, dm.configDigest, dm.blobs, tmpRootFSDir, &privileges, optsList...)
344 351
 	if err != nil {
345 352
 		return err
346 353
 	}
347
-	p.PluginObj.PluginReference = ref.String()
348 354
 
355
+	pm.publisher.Publish(EventCreate{Plugin: p.PluginObj})
349 356
 	return nil
350 357
 }
351 358
 
... ...
@@ -640,6 +649,7 @@ func (pm *Manager) Remove(name string, config *types.PluginRmConfig) error {
640 640
 	}
641 641
 	pm.config.Store.Remove(p)
642 642
 	pm.config.LogPluginEvent(id, name, "remove")
643
+	pm.publisher.Publish(EventRemove{Plugin: p.PluginObj})
643 644
 	return nil
644 645
 }
645 646
 
... ...
@@ -771,6 +781,7 @@ func (pm *Manager) CreateFromContext(ctx context.Context, tarCtx io.ReadCloser,
771 771
 	}
772 772
 	p.PluginObj.PluginReference = name
773 773
 
774
+	pm.publisher.Publish(EventCreate{Plugin: p.PluginObj})
774 775
 	pm.config.LogPluginEvent(p.PluginObj.ID, name, "create")
775 776
 
776 777
 	return nil
... ...
@@ -36,7 +36,7 @@ func (pm *Manager) Privileges(ctx context.Context, ref reference.Named, metaHead
36 36
 }
37 37
 
38 38
 // Pull pulls a plugin, check if the correct privileges are provided and install the plugin.
39
-func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, out io.Writer) error {
39
+func (pm *Manager) Pull(ctx context.Context, ref reference.Named, name string, metaHeader http.Header, authConfig *types.AuthConfig, privileges types.PluginPrivileges, out io.Writer, opts ...CreateOpt) error {
40 40
 	return errNotSupported
41 41
 }
42 42
 
... ...
@@ -24,3 +24,14 @@ func NewStore() *Store {
24 24
 		handlers: make(map[string][]func(string, *plugins.Client)),
25 25
 	}
26 26
 }
27
+
28
+// CreateOpt is used to configure specific plugin details when created
29
+type CreateOpt func(p *v2.Plugin)
30
+
31
+// WithSwarmService is a CreateOpt that flags the passed in a plugin as a plugin
32
+// managed by swarm
33
+func WithSwarmService(id string) CreateOpt {
34
+	return func(p *v2.Plugin) {
35
+		p.SwarmServiceID = id
36
+	}
37
+}
27 38
new file mode 100644
... ...
@@ -0,0 +1,111 @@
0
+package plugin
1
+
2
+import (
3
+	"fmt"
4
+	"reflect"
5
+
6
+	"github.com/docker/docker/api/types"
7
+)
8
+
9
+// Event is emitted for actions performed on the plugin manager
10
+type Event interface {
11
+	matches(Event) bool
12
+}
13
+
14
+// EventCreate is an event which is emitted when a plugin is created
15
+// This is either by pull or create from context.
16
+//
17
+// Use the `Interfaces` field to match only plugins that implement a specific
18
+// interface.
19
+// These are matched against using "or" logic.
20
+// If no interfaces are listed, all are matched.
21
+type EventCreate struct {
22
+	Interfaces map[string]bool
23
+	Plugin     types.Plugin
24
+}
25
+
26
+func (e EventCreate) matches(observed Event) bool {
27
+	oe, ok := observed.(EventCreate)
28
+	if !ok {
29
+		return false
30
+	}
31
+	if len(e.Interfaces) == 0 {
32
+		return true
33
+	}
34
+
35
+	var ifaceMatch bool
36
+	for _, in := range oe.Plugin.Config.Interface.Types {
37
+		if e.Interfaces[in.Capability] {
38
+			ifaceMatch = true
39
+			break
40
+		}
41
+	}
42
+	return ifaceMatch
43
+}
44
+
45
+// EventRemove is an event which is emitted when a plugin is removed
46
+// It maches on the passed in plugin's ID only.
47
+type EventRemove struct {
48
+	Plugin types.Plugin
49
+}
50
+
51
+func (e EventRemove) matches(observed Event) bool {
52
+	oe, ok := observed.(EventRemove)
53
+	if !ok {
54
+		return false
55
+	}
56
+	return e.Plugin.ID == oe.Plugin.ID
57
+}
58
+
59
+// EventDisable is an event that is emitted when a plugin is disabled
60
+// It maches on the passed in plugin's ID only.
61
+type EventDisable struct {
62
+	Plugin types.Plugin
63
+}
64
+
65
+func (e EventDisable) matches(observed Event) bool {
66
+	oe, ok := observed.(EventDisable)
67
+	if !ok {
68
+		return false
69
+	}
70
+	return e.Plugin.ID == oe.Plugin.ID
71
+}
72
+
73
+// EventEnable is an event that is emitted when a plugin is disabled
74
+// It maches on the passed in plugin's ID only.
75
+type EventEnable struct {
76
+	Plugin types.Plugin
77
+}
78
+
79
+func (e EventEnable) matches(observed Event) bool {
80
+	oe, ok := observed.(EventEnable)
81
+	if !ok {
82
+		return false
83
+	}
84
+	return e.Plugin.ID == oe.Plugin.ID
85
+}
86
+
87
+// SubscribeEvents provides an event channel to listen for structured events from
88
+// the plugin manager actions, CRUD operations.
89
+// The caller must call the returned `cancel()` function once done with the channel
90
+// or this will leak resources.
91
+func (pm *Manager) SubscribeEvents(buffer int, watchEvents ...Event) (eventCh <-chan interface{}, cancel func()) {
92
+	topic := func(i interface{}) bool {
93
+		observed, ok := i.(Event)
94
+		if !ok {
95
+			panic(fmt.Sprintf("unexpected type passed to event channel: %v", reflect.TypeOf(i)))
96
+		}
97
+		for _, e := range watchEvents {
98
+			if e.matches(observed) {
99
+				return true
100
+			}
101
+		}
102
+		// If no specific events are specified always assume a matched event
103
+		// If some events were specified and none matched above, then the event
104
+		// doesn't match
105
+		return watchEvents == nil
106
+	}
107
+	ch := pm.publisher.SubscribeTopicWithBuffer(topic, buffer)
108
+	cancelFunc := func() { pm.publisher.Evict(ch) }
109
+	return ch, cancelFunc
110
+}
... ...
@@ -22,6 +22,7 @@ import (
22 22
 	"github.com/docker/docker/pkg/authorization"
23 23
 	"github.com/docker/docker/pkg/ioutils"
24 24
 	"github.com/docker/docker/pkg/mount"
25
+	"github.com/docker/docker/pkg/pubsub"
25 26
 	"github.com/docker/docker/pkg/system"
26 27
 	"github.com/docker/docker/plugin/v2"
27 28
 	"github.com/docker/docker/registry"
... ...
@@ -63,6 +64,7 @@ type Manager struct {
63 63
 	cMap             map[*v2.Plugin]*controller
64 64
 	containerdClient libcontainerd.Client
65 65
 	blobStore        *basicBlobStore
66
+	publisher        *pubsub.Publisher
66 67
 }
67 68
 
68 69
 // controller represents the manager's control on a plugin.
... ...
@@ -117,6 +119,8 @@ func NewManager(config ManagerConfig) (*Manager, error) {
117 117
 	if err := manager.reload(); err != nil {
118 118
 		return nil, errors.Wrap(err, "failed to restore plugins")
119 119
 	}
120
+
121
+	manager.publisher = pubsub.NewPublisher(0, 0)
120 122
 	return manager, nil
121 123
 }
122 124
 
... ...
@@ -268,6 +272,11 @@ func (pm *Manager) reload() error { // todo: restore
268 268
 	return nil
269 269
 }
270 270
 
271
+// Get looks up the requested plugin in the store.
272
+func (pm *Manager) Get(idOrName string) (*v2.Plugin, error) {
273
+	return pm.config.Store.GetV2Plugin(idOrName)
274
+}
275
+
271 276
 func (pm *Manager) loadPlugin(id string) (*v2.Plugin, error) {
272 277
 	p := filepath.Join(pm.config.Root, id, configFileName)
273 278
 	dt, err := ioutil.ReadFile(p)
... ...
@@ -274,7 +274,7 @@ func (pm *Manager) setupNewPlugin(configDigest digest.Digest, blobsums []digest.
274 274
 }
275 275
 
276 276
 // createPlugin creates a new plugin. take lock before calling.
277
-func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges) (p *v2.Plugin, err error) {
277
+func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsums []digest.Digest, rootFSDir string, privileges *types.PluginPrivileges, opts ...CreateOpt) (p *v2.Plugin, err error) {
278 278
 	if err := pm.config.Store.validateName(name); err != nil { // todo: this check is wrong. remove store
279 279
 		return nil, err
280 280
 	}
... ...
@@ -294,6 +294,9 @@ func (pm *Manager) createPlugin(name string, configDigest digest.Digest, blobsum
294 294
 		Blobsums: blobsums,
295 295
 	}
296 296
 	p.InitEmptySettings()
297
+	for _, o := range opts {
298
+		o(p)
299
+	}
297 300
 
298 301
 	pdir := filepath.Join(pm.config.Root, p.PluginObj.ID)
299 302
 	if err := os.MkdirAll(pdir, 0700); err != nil {
... ...
@@ -22,6 +22,8 @@ type Plugin struct {
22 22
 
23 23
 	Config   digest.Digest
24 24
 	Blobsums []digest.Digest
25
+
26
+	SwarmServiceID string
25 27
 }
26 28
 
27 29
 const defaultPluginRuntimeDestination = "/run/docker/plugins"