1package convert // import "github.com/docker/docker/daemon/cluster/convert"
2
3import (
4	"fmt"
5	"strings"
6
7	types "github.com/docker/docker/api/types/swarm"
8	"github.com/docker/docker/api/types/swarm/runtime"
9	"github.com/docker/docker/pkg/namesgenerator"
10	swarmapi "github.com/docker/swarmkit/api"
11	"github.com/docker/swarmkit/api/genericresource"
12	"github.com/gogo/protobuf/proto"
13	gogotypes "github.com/gogo/protobuf/types"
14	"github.com/pkg/errors"
15)
16
17var (
18	// ErrUnsupportedRuntime returns an error if the runtime is not supported by the daemon
19	ErrUnsupportedRuntime = errors.New("unsupported runtime")
20	// ErrMismatchedRuntime returns an error if the runtime does not match the provided spec
21	ErrMismatchedRuntime = errors.New("mismatched Runtime and *Spec fields")
22)
23
24// ServiceFromGRPC converts a grpc Service to a Service.
25func ServiceFromGRPC(s swarmapi.Service) (types.Service, error) {
26	curSpec, err := serviceSpecFromGRPC(&s.Spec)
27	if err != nil {
28		return types.Service{}, err
29	}
30	prevSpec, err := serviceSpecFromGRPC(s.PreviousSpec)
31	if err != nil {
32		return types.Service{}, err
33	}
34	service := types.Service{
35		ID:           s.ID,
36		Spec:         *curSpec,
37		PreviousSpec: prevSpec,
38
39		Endpoint: endpointFromGRPC(s.Endpoint),
40	}
41
42	// Meta
43	service.Version.Index = s.Meta.Version.Index
44	service.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
45	service.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
46
47	// UpdateStatus
48	if s.UpdateStatus != nil {
49		service.UpdateStatus = &types.UpdateStatus{}
50		switch s.UpdateStatus.State {
51		case swarmapi.UpdateStatus_UPDATING:
52			service.UpdateStatus.State = types.UpdateStateUpdating
53		case swarmapi.UpdateStatus_PAUSED:
54			service.UpdateStatus.State = types.UpdateStatePaused
55		case swarmapi.UpdateStatus_COMPLETED:
56			service.UpdateStatus.State = types.UpdateStateCompleted
57		case swarmapi.UpdateStatus_ROLLBACK_STARTED:
58			service.UpdateStatus.State = types.UpdateStateRollbackStarted
59		case swarmapi.UpdateStatus_ROLLBACK_PAUSED:
60			service.UpdateStatus.State = types.UpdateStateRollbackPaused
61		case swarmapi.UpdateStatus_ROLLBACK_COMPLETED:
62			service.UpdateStatus.State = types.UpdateStateRollbackCompleted
63		}
64
65		startedAt, _ := gogotypes.TimestampFromProto(s.UpdateStatus.StartedAt)
66		if !startedAt.IsZero() && startedAt.Unix() != 0 {
67			service.UpdateStatus.StartedAt = &startedAt
68		}
69
70		completedAt, _ := gogotypes.TimestampFromProto(s.UpdateStatus.CompletedAt)
71		if !completedAt.IsZero() && completedAt.Unix() != 0 {
72			service.UpdateStatus.CompletedAt = &completedAt
73		}
74
75		service.UpdateStatus.Message = s.UpdateStatus.Message
76	}
77
78	return service, nil
79}
80
81func serviceSpecFromGRPC(spec *swarmapi.ServiceSpec) (*types.ServiceSpec, error) {
82	if spec == nil {
83		return nil, nil
84	}
85
86	serviceNetworks := make([]types.NetworkAttachmentConfig, 0, len(spec.Networks))
87	for _, n := range spec.Networks {
88		netConfig := types.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverOpts: n.DriverAttachmentOpts}
89		serviceNetworks = append(serviceNetworks, netConfig)
90
91	}
92
93	taskTemplate, err := taskSpecFromGRPC(spec.Task)
94	if err != nil {
95		return nil, err
96	}
97
98	switch t := spec.Task.GetRuntime().(type) {
99	case *swarmapi.TaskSpec_Container:
100		containerConfig := t.Container
101		taskTemplate.ContainerSpec = containerSpecFromGRPC(containerConfig)
102		taskTemplate.Runtime = types.RuntimeContainer
103	case *swarmapi.TaskSpec_Generic:
104		switch t.Generic.Kind {
105		case string(types.RuntimePlugin):
106			taskTemplate.Runtime = types.RuntimePlugin
107		default:
108			return nil, fmt.Errorf("unknown task runtime type: %s", t.Generic.Payload.TypeUrl)
109		}
110
111	default:
112		return nil, fmt.Errorf("error creating service; unsupported runtime %T", t)
113	}
114
115	convertedSpec := &types.ServiceSpec{
116		Annotations:  annotationsFromGRPC(spec.Annotations),
117		TaskTemplate: taskTemplate,
118		Networks:     serviceNetworks,
119		EndpointSpec: endpointSpecFromGRPC(spec.Endpoint),
120	}
121
122	// UpdateConfig
123	convertedSpec.UpdateConfig = updateConfigFromGRPC(spec.Update)
124	convertedSpec.RollbackConfig = updateConfigFromGRPC(spec.Rollback)
125
126	// Mode
127	switch t := spec.GetMode().(type) {
128	case *swarmapi.ServiceSpec_Global:
129		convertedSpec.Mode.Global = &types.GlobalService{}
130	case *swarmapi.ServiceSpec_Replicated:
131		convertedSpec.Mode.Replicated = &types.ReplicatedService{
132			Replicas: &t.Replicated.Replicas,
133		}
134	}
135
136	return convertedSpec, nil
137}
138
139// ServiceSpecToGRPC converts a ServiceSpec to a grpc ServiceSpec.
140func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
141	name := s.Name
142	if name == "" {
143		name = namesgenerator.GetRandomName(0)
144	}
145
146	serviceNetworks := make([]*swarmapi.NetworkAttachmentConfig, 0, len(s.Networks))
147	for _, n := range s.Networks {
148		netConfig := &swarmapi.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverAttachmentOpts: n.DriverOpts}
149		serviceNetworks = append(serviceNetworks, netConfig)
150	}
151
152	taskNetworks := make([]*swarmapi.NetworkAttachmentConfig, 0, len(s.TaskTemplate.Networks))
153	for _, n := range s.TaskTemplate.Networks {
154		netConfig := &swarmapi.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverAttachmentOpts: n.DriverOpts}
155		taskNetworks = append(taskNetworks, netConfig)
156
157	}
158
159	spec := swarmapi.ServiceSpec{
160		Annotations: swarmapi.Annotations{
161			Name:   name,
162			Labels: s.Labels,
163		},
164		Task: swarmapi.TaskSpec{
165			Resources:   resourcesToGRPC(s.TaskTemplate.Resources),
166			LogDriver:   driverToGRPC(s.TaskTemplate.LogDriver),
167			Networks:    taskNetworks,
168			ForceUpdate: s.TaskTemplate.ForceUpdate,
169		},
170		Networks: serviceNetworks,
171	}
172
173	switch s.TaskTemplate.Runtime {
174	case types.RuntimeContainer, "": // if empty runtime default to container
175		if s.TaskTemplate.ContainerSpec != nil {
176			containerSpec, err := containerToGRPC(s.TaskTemplate.ContainerSpec)
177			if err != nil {
178				return swarmapi.ServiceSpec{}, err
179			}
180			spec.Task.Runtime = &swarmapi.TaskSpec_Container{Container: containerSpec}
181		} else {
182			// If the ContainerSpec is nil, we can't set the task runtime
183			return swarmapi.ServiceSpec{}, ErrMismatchedRuntime
184		}
185	case types.RuntimePlugin:
186		if s.TaskTemplate.PluginSpec != nil {
187			if s.Mode.Replicated != nil {
188				return swarmapi.ServiceSpec{}, errors.New("plugins must not use replicated mode")
189			}
190
191			s.Mode.Global = &types.GlobalService{} // must always be global
192
193			pluginSpec, err := proto.Marshal(s.TaskTemplate.PluginSpec)
194			if err != nil {
195				return swarmapi.ServiceSpec{}, err
196			}
197			spec.Task.Runtime = &swarmapi.TaskSpec_Generic{
198				Generic: &swarmapi.GenericRuntimeSpec{
199					Kind: string(types.RuntimePlugin),
200					Payload: &gogotypes.Any{
201						TypeUrl: string(types.RuntimeURLPlugin),
202						Value:   pluginSpec,
203					},
204				},
205			}
206		} else {
207			return swarmapi.ServiceSpec{}, ErrMismatchedRuntime
208		}
209	case types.RuntimeNetworkAttachment:
210		// NOTE(dperny) I'm leaving this case here for completeness. The actual
211		// code is left out out deliberately, as we should refuse to parse a
212		// Network Attachment runtime; it will cause weird behavior all over
213		// the system if we do. Instead, fallthrough and return
214		// ErrUnsupportedRuntime if we get one.
215		fallthrough
216	default:
217		return swarmapi.ServiceSpec{}, ErrUnsupportedRuntime
218	}
219
220	restartPolicy, err := restartPolicyToGRPC(s.TaskTemplate.RestartPolicy)
221	if err != nil {
222		return swarmapi.ServiceSpec{}, err
223	}
224	spec.Task.Restart = restartPolicy
225
226	if s.TaskTemplate.Placement != nil {
227		var preferences []*swarmapi.PlacementPreference
228		for _, pref := range s.TaskTemplate.Placement.Preferences {
229			if pref.Spread != nil {
230				preferences = append(preferences, &swarmapi.PlacementPreference{
231					Preference: &swarmapi.PlacementPreference_Spread{
232						Spread: &swarmapi.SpreadOver{
233							SpreadDescriptor: pref.Spread.SpreadDescriptor,
234						},
235					},
236				})
237			}
238		}
239		var platforms []*swarmapi.Platform
240		for _, plat := range s.TaskTemplate.Placement.Platforms {
241			platforms = append(platforms, &swarmapi.Platform{
242				Architecture: plat.Architecture,
243				OS:           plat.OS,
244			})
245		}
246		spec.Task.Placement = &swarmapi.Placement{
247			Constraints: s.TaskTemplate.Placement.Constraints,
248			Preferences: preferences,
249			Platforms:   platforms,
250		}
251	}
252
253	spec.Update, err = updateConfigToGRPC(s.UpdateConfig)
254	if err != nil {
255		return swarmapi.ServiceSpec{}, err
256	}
257	spec.Rollback, err = updateConfigToGRPC(s.RollbackConfig)
258	if err != nil {
259		return swarmapi.ServiceSpec{}, err
260	}
261
262	if s.EndpointSpec != nil {
263		if s.EndpointSpec.Mode != "" &&
264			s.EndpointSpec.Mode != types.ResolutionModeVIP &&
265			s.EndpointSpec.Mode != types.ResolutionModeDNSRR {
266			return swarmapi.ServiceSpec{}, fmt.Errorf("invalid resolution mode: %q", s.EndpointSpec.Mode)
267		}
268
269		spec.Endpoint = &swarmapi.EndpointSpec{}
270
271		spec.Endpoint.Mode = swarmapi.EndpointSpec_ResolutionMode(swarmapi.EndpointSpec_ResolutionMode_value[strings.ToUpper(string(s.EndpointSpec.Mode))])
272
273		for _, portConfig := range s.EndpointSpec.Ports {
274			spec.Endpoint.Ports = append(spec.Endpoint.Ports, &swarmapi.PortConfig{
275				Name:          portConfig.Name,
276				Protocol:      swarmapi.PortConfig_Protocol(swarmapi.PortConfig_Protocol_value[strings.ToUpper(string(portConfig.Protocol))]),
277				PublishMode:   swarmapi.PortConfig_PublishMode(swarmapi.PortConfig_PublishMode_value[strings.ToUpper(string(portConfig.PublishMode))]),
278				TargetPort:    portConfig.TargetPort,
279				PublishedPort: portConfig.PublishedPort,
280			})
281		}
282	}
283
284	// Mode
285	if s.Mode.Global != nil && s.Mode.Replicated != nil {
286		return swarmapi.ServiceSpec{}, fmt.Errorf("cannot specify both replicated mode and global mode")
287	}
288
289	if s.Mode.Global != nil {
290		spec.Mode = &swarmapi.ServiceSpec_Global{
291			Global: &swarmapi.GlobalService{},
292		}
293	} else if s.Mode.Replicated != nil && s.Mode.Replicated.Replicas != nil {
294		spec.Mode = &swarmapi.ServiceSpec_Replicated{
295			Replicated: &swarmapi.ReplicatedService{Replicas: *s.Mode.Replicated.Replicas},
296		}
297	} else {
298		spec.Mode = &swarmapi.ServiceSpec_Replicated{
299			Replicated: &swarmapi.ReplicatedService{Replicas: 1},
300		}
301	}
302
303	return spec, nil
304}
305
306func annotationsFromGRPC(ann swarmapi.Annotations) types.Annotations {
307	a := types.Annotations{
308		Name:   ann.Name,
309		Labels: ann.Labels,
310	}
311
312	if a.Labels == nil {
313		a.Labels = make(map[string]string)
314	}
315
316	return a
317}
318
319// GenericResourcesFromGRPC converts a GRPC GenericResource to a GenericResource
320func GenericResourcesFromGRPC(genericRes []*swarmapi.GenericResource) []types.GenericResource {
321	var generic []types.GenericResource
322	for _, res := range genericRes {
323		var current types.GenericResource
324
325		switch r := res.Resource.(type) {
326		case *swarmapi.GenericResource_DiscreteResourceSpec:
327			current.DiscreteResourceSpec = &types.DiscreteGenericResource{
328				Kind:  r.DiscreteResourceSpec.Kind,
329				Value: r.DiscreteResourceSpec.Value,
330			}
331		case *swarmapi.GenericResource_NamedResourceSpec:
332			current.NamedResourceSpec = &types.NamedGenericResource{
333				Kind:  r.NamedResourceSpec.Kind,
334				Value: r.NamedResourceSpec.Value,
335			}
336		}
337
338		generic = append(generic, current)
339	}
340
341	return generic
342}
343
344func resourcesFromGRPC(res *swarmapi.ResourceRequirements) *types.ResourceRequirements {
345	var resources *types.ResourceRequirements
346	if res != nil {
347		resources = &types.ResourceRequirements{}
348		if res.Limits != nil {
349			resources.Limits = &types.Resources{
350				NanoCPUs:    res.Limits.NanoCPUs,
351				MemoryBytes: res.Limits.MemoryBytes,
352			}
353		}
354		if res.Reservations != nil {
355			resources.Reservations = &types.Resources{
356				NanoCPUs:         res.Reservations.NanoCPUs,
357				MemoryBytes:      res.Reservations.MemoryBytes,
358				GenericResources: GenericResourcesFromGRPC(res.Reservations.Generic),
359			}
360		}
361	}
362
363	return resources
364}
365
366// GenericResourcesToGRPC converts a GenericResource to a GRPC GenericResource
367func GenericResourcesToGRPC(genericRes []types.GenericResource) []*swarmapi.GenericResource {
368	var generic []*swarmapi.GenericResource
369	for _, res := range genericRes {
370		var r *swarmapi.GenericResource
371
372		if res.DiscreteResourceSpec != nil {
373			r = genericresource.NewDiscrete(res.DiscreteResourceSpec.Kind, res.DiscreteResourceSpec.Value)
374		} else if res.NamedResourceSpec != nil {
375			r = genericresource.NewString(res.NamedResourceSpec.Kind, res.NamedResourceSpec.Value)
376		}
377
378		generic = append(generic, r)
379	}
380
381	return generic
382}
383
384func resourcesToGRPC(res *types.ResourceRequirements) *swarmapi.ResourceRequirements {
385	var reqs *swarmapi.ResourceRequirements
386	if res != nil {
387		reqs = &swarmapi.ResourceRequirements{}
388		if res.Limits != nil {
389			reqs.Limits = &swarmapi.Resources{
390				NanoCPUs:    res.Limits.NanoCPUs,
391				MemoryBytes: res.Limits.MemoryBytes,
392			}
393		}
394		if res.Reservations != nil {
395			reqs.Reservations = &swarmapi.Resources{
396				NanoCPUs:    res.Reservations.NanoCPUs,
397				MemoryBytes: res.Reservations.MemoryBytes,
398				Generic:     GenericResourcesToGRPC(res.Reservations.GenericResources),
399			}
400
401		}
402	}
403	return reqs
404}
405
406func restartPolicyFromGRPC(p *swarmapi.RestartPolicy) *types.RestartPolicy {
407	var rp *types.RestartPolicy
408	if p != nil {
409		rp = &types.RestartPolicy{}
410
411		switch p.Condition {
412		case swarmapi.RestartOnNone:
413			rp.Condition = types.RestartPolicyConditionNone
414		case swarmapi.RestartOnFailure:
415			rp.Condition = types.RestartPolicyConditionOnFailure
416		case swarmapi.RestartOnAny:
417			rp.Condition = types.RestartPolicyConditionAny
418		default:
419			rp.Condition = types.RestartPolicyConditionAny
420		}
421
422		if p.Delay != nil {
423			delay, _ := gogotypes.DurationFromProto(p.Delay)
424			rp.Delay = &delay
425		}
426		if p.Window != nil {
427			window, _ := gogotypes.DurationFromProto(p.Window)
428			rp.Window = &window
429		}
430
431		rp.MaxAttempts = &p.MaxAttempts
432	}
433	return rp
434}
435
436func restartPolicyToGRPC(p *types.RestartPolicy) (*swarmapi.RestartPolicy, error) {
437	var rp *swarmapi.RestartPolicy
438	if p != nil {
439		rp = &swarmapi.RestartPolicy{}
440
441		switch p.Condition {
442		case types.RestartPolicyConditionNone:
443			rp.Condition = swarmapi.RestartOnNone
444		case types.RestartPolicyConditionOnFailure:
445			rp.Condition = swarmapi.RestartOnFailure
446		case types.RestartPolicyConditionAny:
447			rp.Condition = swarmapi.RestartOnAny
448		default:
449			if string(p.Condition) != "" {
450				return nil, fmt.Errorf("invalid RestartCondition: %q", p.Condition)
451			}
452			rp.Condition = swarmapi.RestartOnAny
453		}
454
455		if p.Delay != nil {
456			rp.Delay = gogotypes.DurationProto(*p.Delay)
457		}
458		if p.Window != nil {
459			rp.Window = gogotypes.DurationProto(*p.Window)
460		}
461		if p.MaxAttempts != nil {
462			rp.MaxAttempts = *p.MaxAttempts
463
464		}
465	}
466	return rp, nil
467}
468
469func placementFromGRPC(p *swarmapi.Placement) *types.Placement {
470	if p == nil {
471		return nil
472	}
473	r := &types.Placement{
474		Constraints: p.Constraints,
475	}
476
477	for _, pref := range p.Preferences {
478		if spread := pref.GetSpread(); spread != nil {
479			r.Preferences = append(r.Preferences, types.PlacementPreference{
480				Spread: &types.SpreadOver{
481					SpreadDescriptor: spread.SpreadDescriptor,
482				},
483			})
484		}
485	}
486
487	for _, plat := range p.Platforms {
488		r.Platforms = append(r.Platforms, types.Platform{
489			Architecture: plat.Architecture,
490			OS:           plat.OS,
491		})
492	}
493
494	return r
495}
496
497func driverFromGRPC(p *swarmapi.Driver) *types.Driver {
498	if p == nil {
499		return nil
500	}
501
502	return &types.Driver{
503		Name:    p.Name,
504		Options: p.Options,
505	}
506}
507
508func driverToGRPC(p *types.Driver) *swarmapi.Driver {
509	if p == nil {
510		return nil
511	}
512
513	return &swarmapi.Driver{
514		Name:    p.Name,
515		Options: p.Options,
516	}
517}
518
519func updateConfigFromGRPC(updateConfig *swarmapi.UpdateConfig) *types.UpdateConfig {
520	if updateConfig == nil {
521		return nil
522	}
523
524	converted := &types.UpdateConfig{
525		Parallelism:     updateConfig.Parallelism,
526		MaxFailureRatio: updateConfig.MaxFailureRatio,
527	}
528
529	converted.Delay = updateConfig.Delay
530	if updateConfig.Monitor != nil {
531		converted.Monitor, _ = gogotypes.DurationFromProto(updateConfig.Monitor)
532	}
533
534	switch updateConfig.FailureAction {
535	case swarmapi.UpdateConfig_PAUSE:
536		converted.FailureAction = types.UpdateFailureActionPause
537	case swarmapi.UpdateConfig_CONTINUE:
538		converted.FailureAction = types.UpdateFailureActionContinue
539	case swarmapi.UpdateConfig_ROLLBACK:
540		converted.FailureAction = types.UpdateFailureActionRollback
541	}
542
543	switch updateConfig.Order {
544	case swarmapi.UpdateConfig_STOP_FIRST:
545		converted.Order = types.UpdateOrderStopFirst
546	case swarmapi.UpdateConfig_START_FIRST:
547		converted.Order = types.UpdateOrderStartFirst
548	}
549
550	return converted
551}
552
553func updateConfigToGRPC(updateConfig *types.UpdateConfig) (*swarmapi.UpdateConfig, error) {
554	if updateConfig == nil {
555		return nil, nil
556	}
557
558	converted := &swarmapi.UpdateConfig{
559		Parallelism:     updateConfig.Parallelism,
560		Delay:           updateConfig.Delay,
561		MaxFailureRatio: updateConfig.MaxFailureRatio,
562	}
563
564	switch updateConfig.FailureAction {
565	case types.UpdateFailureActionPause, "":
566		converted.FailureAction = swarmapi.UpdateConfig_PAUSE
567	case types.UpdateFailureActionContinue:
568		converted.FailureAction = swarmapi.UpdateConfig_CONTINUE
569	case types.UpdateFailureActionRollback:
570		converted.FailureAction = swarmapi.UpdateConfig_ROLLBACK
571	default:
572		return nil, fmt.Errorf("unrecognized update failure action %s", updateConfig.FailureAction)
573	}
574	if updateConfig.Monitor != 0 {
575		converted.Monitor = gogotypes.DurationProto(updateConfig.Monitor)
576	}
577
578	switch updateConfig.Order {
579	case types.UpdateOrderStopFirst, "":
580		converted.Order = swarmapi.UpdateConfig_STOP_FIRST
581	case types.UpdateOrderStartFirst:
582		converted.Order = swarmapi.UpdateConfig_START_FIRST
583	default:
584		return nil, fmt.Errorf("unrecognized update order %s", updateConfig.Order)
585	}
586
587	return converted, nil
588}
589
590func networkAttachmentSpecFromGRPC(attachment swarmapi.NetworkAttachmentSpec) *types.NetworkAttachmentSpec {
591	return &types.NetworkAttachmentSpec{
592		ContainerID: attachment.ContainerID,
593	}
594}
595
596func taskSpecFromGRPC(taskSpec swarmapi.TaskSpec) (types.TaskSpec, error) {
597	taskNetworks := make([]types.NetworkAttachmentConfig, 0, len(taskSpec.Networks))
598	for _, n := range taskSpec.Networks {
599		netConfig := types.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverOpts: n.DriverAttachmentOpts}
600		taskNetworks = append(taskNetworks, netConfig)
601	}
602
603	t := types.TaskSpec{
604		Resources:     resourcesFromGRPC(taskSpec.Resources),
605		RestartPolicy: restartPolicyFromGRPC(taskSpec.Restart),
606		Placement:     placementFromGRPC(taskSpec.Placement),
607		LogDriver:     driverFromGRPC(taskSpec.LogDriver),
608		Networks:      taskNetworks,
609		ForceUpdate:   taskSpec.ForceUpdate,
610	}
611
612	switch taskSpec.GetRuntime().(type) {
613	case *swarmapi.TaskSpec_Container, nil:
614		c := taskSpec.GetContainer()
615		if c != nil {
616			t.ContainerSpec = containerSpecFromGRPC(c)
617		}
618	case *swarmapi.TaskSpec_Generic:
619		g := taskSpec.GetGeneric()
620		if g != nil {
621			switch g.Kind {
622			case string(types.RuntimePlugin):
623				var p runtime.PluginSpec
624				if err := proto.Unmarshal(g.Payload.Value, &p); err != nil {
625					return t, errors.Wrap(err, "error unmarshalling plugin spec")
626				}
627				t.PluginSpec = &p
628			}
629		}
630	case *swarmapi.TaskSpec_Attachment:
631		a := taskSpec.GetAttachment()
632		if a != nil {
633			t.NetworkAttachmentSpec = networkAttachmentSpecFromGRPC(*a)
634		}
635		t.Runtime = types.RuntimeNetworkAttachment
636	}
637
638	return t, nil
639}
640