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>
| ... | ... |
@@ -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." |
| 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 {
|