dotcloud/docker

View on GitHub
daemon/cluster/convert/service.go

Summary

Maintainability
D
2 days
Test Coverage
package convert // import "github.com/docker/docker/daemon/cluster/convert"

import (
    "fmt"
    "strings"

    types "github.com/docker/docker/api/types/swarm"
    "github.com/docker/docker/api/types/swarm/runtime"
    "github.com/docker/docker/pkg/namesgenerator"
    "github.com/gogo/protobuf/proto"
    gogotypes "github.com/gogo/protobuf/types"
    swarmapi "github.com/moby/swarmkit/v2/api"
    "github.com/moby/swarmkit/v2/api/genericresource"
    "github.com/pkg/errors"
)

var (
    // ErrUnsupportedRuntime returns an error if the runtime is not supported by the daemon
    ErrUnsupportedRuntime = errors.New("unsupported runtime")
    // ErrMismatchedRuntime returns an error if the runtime does not match the provided spec
    ErrMismatchedRuntime = errors.New("mismatched Runtime and *Spec fields")
)

// ServiceFromGRPC converts a grpc Service to a Service.
func ServiceFromGRPC(s swarmapi.Service) (types.Service, error) {
    curSpec, err := serviceSpecFromGRPC(&s.Spec)
    if err != nil {
        return types.Service{}, err
    }
    prevSpec, err := serviceSpecFromGRPC(s.PreviousSpec)
    if err != nil {
        return types.Service{}, err
    }
    service := types.Service{
        ID:           s.ID,
        Spec:         *curSpec,
        PreviousSpec: prevSpec,

        Endpoint: endpointFromGRPC(s.Endpoint),
    }

    // Meta
    service.Version.Index = s.Meta.Version.Index
    service.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
    service.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)

    if s.JobStatus != nil {
        service.JobStatus = &types.JobStatus{
            JobIteration: types.Version{
                Index: s.JobStatus.JobIteration.Index,
            },
        }
        service.JobStatus.LastExecution, _ = gogotypes.TimestampFromProto(s.JobStatus.LastExecution)
    }

    // UpdateStatus
    if s.UpdateStatus != nil {
        service.UpdateStatus = &types.UpdateStatus{}
        switch s.UpdateStatus.State {
        case swarmapi.UpdateStatus_UPDATING:
            service.UpdateStatus.State = types.UpdateStateUpdating
        case swarmapi.UpdateStatus_PAUSED:
            service.UpdateStatus.State = types.UpdateStatePaused
        case swarmapi.UpdateStatus_COMPLETED:
            service.UpdateStatus.State = types.UpdateStateCompleted
        case swarmapi.UpdateStatus_ROLLBACK_STARTED:
            service.UpdateStatus.State = types.UpdateStateRollbackStarted
        case swarmapi.UpdateStatus_ROLLBACK_PAUSED:
            service.UpdateStatus.State = types.UpdateStateRollbackPaused
        case swarmapi.UpdateStatus_ROLLBACK_COMPLETED:
            service.UpdateStatus.State = types.UpdateStateRollbackCompleted
        }

        startedAt, _ := gogotypes.TimestampFromProto(s.UpdateStatus.StartedAt)
        if !startedAt.IsZero() && startedAt.Unix() != 0 {
            service.UpdateStatus.StartedAt = &startedAt
        }

        completedAt, _ := gogotypes.TimestampFromProto(s.UpdateStatus.CompletedAt)
        if !completedAt.IsZero() && completedAt.Unix() != 0 {
            service.UpdateStatus.CompletedAt = &completedAt
        }

        service.UpdateStatus.Message = s.UpdateStatus.Message
    }

    return service, nil
}

func serviceSpecFromGRPC(spec *swarmapi.ServiceSpec) (*types.ServiceSpec, error) {
    if spec == nil {
        return nil, nil
    }

    serviceNetworks := make([]types.NetworkAttachmentConfig, 0, len(spec.Networks))
    for _, n := range spec.Networks {
        netConfig := types.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverOpts: n.DriverAttachmentOpts}
        serviceNetworks = append(serviceNetworks, netConfig)
    }

    taskTemplate, err := taskSpecFromGRPC(spec.Task)
    if err != nil {
        return nil, err
    }

    switch t := spec.Task.GetRuntime().(type) {
    case *swarmapi.TaskSpec_Container:
        containerConfig := t.Container
        taskTemplate.ContainerSpec = containerSpecFromGRPC(containerConfig)
        taskTemplate.Runtime = types.RuntimeContainer
    case *swarmapi.TaskSpec_Generic:
        switch t.Generic.Kind {
        case string(types.RuntimePlugin):
            taskTemplate.Runtime = types.RuntimePlugin
        default:
            return nil, fmt.Errorf("unknown task runtime type: %s", t.Generic.Payload.TypeUrl)
        }

    default:
        return nil, fmt.Errorf("error creating service; unsupported runtime %T", t)
    }

    convertedSpec := &types.ServiceSpec{
        Annotations:  annotationsFromGRPC(spec.Annotations),
        TaskTemplate: taskTemplate,
        Networks:     serviceNetworks,
        EndpointSpec: endpointSpecFromGRPC(spec.Endpoint),
    }

    // UpdateConfig
    convertedSpec.UpdateConfig = updateConfigFromGRPC(spec.Update)
    convertedSpec.RollbackConfig = updateConfigFromGRPC(spec.Rollback)

    // Mode
    switch t := spec.GetMode().(type) {
    case *swarmapi.ServiceSpec_Global:
        convertedSpec.Mode.Global = &types.GlobalService{}
    case *swarmapi.ServiceSpec_Replicated:
        convertedSpec.Mode.Replicated = &types.ReplicatedService{
            Replicas: &t.Replicated.Replicas,
        }
    case *swarmapi.ServiceSpec_ReplicatedJob:
        convertedSpec.Mode.ReplicatedJob = &types.ReplicatedJob{
            MaxConcurrent:    &t.ReplicatedJob.MaxConcurrent,
            TotalCompletions: &t.ReplicatedJob.TotalCompletions,
        }
    case *swarmapi.ServiceSpec_GlobalJob:
        convertedSpec.Mode.GlobalJob = &types.GlobalJob{}
    }

    return convertedSpec, nil
}

// ServiceSpecToGRPC converts a ServiceSpec to a grpc ServiceSpec.
func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
    name := s.Name
    if name == "" {
        name = namesgenerator.GetRandomName(0)
    }

    serviceNetworks := make([]*swarmapi.NetworkAttachmentConfig, 0, len(s.Networks)) //nolint:staticcheck // ignore SA1019: field is deprecated.
    for _, n := range s.Networks {                                                   //nolint:staticcheck // ignore SA1019: field is deprecated.
        netConfig := &swarmapi.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverAttachmentOpts: n.DriverOpts}
        serviceNetworks = append(serviceNetworks, netConfig)
    }

    taskNetworks := make([]*swarmapi.NetworkAttachmentConfig, 0, len(s.TaskTemplate.Networks))
    for _, n := range s.TaskTemplate.Networks {
        netConfig := &swarmapi.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverAttachmentOpts: n.DriverOpts}
        taskNetworks = append(taskNetworks, netConfig)
    }

    spec := swarmapi.ServiceSpec{
        Annotations: swarmapi.Annotations{
            Name:   name,
            Labels: s.Labels,
        },
        Task: swarmapi.TaskSpec{
            Resources:   resourcesToGRPC(s.TaskTemplate.Resources),
            LogDriver:   driverToGRPC(s.TaskTemplate.LogDriver),
            Networks:    taskNetworks,
            ForceUpdate: s.TaskTemplate.ForceUpdate,
        },
        Networks: serviceNetworks,
    }

    switch s.TaskTemplate.Runtime {
    case types.RuntimeContainer, "": // if empty runtime default to container
        if s.TaskTemplate.ContainerSpec != nil {
            containerSpec, err := containerToGRPC(s.TaskTemplate.ContainerSpec)
            if err != nil {
                return swarmapi.ServiceSpec{}, err
            }
            if s.TaskTemplate.Resources != nil && s.TaskTemplate.Resources.Limits != nil {
                // TODO remove this (or keep for backward compat) once SwarmKit API moved PidsLimit into Resources
                containerSpec.PidsLimit = s.TaskTemplate.Resources.Limits.Pids
            }
            spec.Task.Runtime = &swarmapi.TaskSpec_Container{Container: containerSpec}
        } else {
            // If the ContainerSpec is nil, we can't set the task runtime
            return swarmapi.ServiceSpec{}, ErrMismatchedRuntime
        }
    case types.RuntimePlugin:
        if s.TaskTemplate.PluginSpec != nil {
            if s.Mode.Replicated != nil {
                return swarmapi.ServiceSpec{}, errors.New("plugins must not use replicated mode")
            }

            s.Mode.Global = &types.GlobalService{} // must always be global

            pluginSpec, err := proto.Marshal(s.TaskTemplate.PluginSpec)
            if err != nil {
                return swarmapi.ServiceSpec{}, err
            }
            spec.Task.Runtime = &swarmapi.TaskSpec_Generic{
                Generic: &swarmapi.GenericRuntimeSpec{
                    Kind: string(types.RuntimePlugin),
                    Payload: &gogotypes.Any{
                        TypeUrl: string(types.RuntimeURLPlugin),
                        Value:   pluginSpec,
                    },
                },
            }
        } else {
            return swarmapi.ServiceSpec{}, ErrMismatchedRuntime
        }
    case types.RuntimeNetworkAttachment:
        // NOTE(dperny) I'm leaving this case here for completeness. The actual
        // code is left out deliberately, as we should refuse to parse a
        // Network Attachment runtime; it will cause weird behavior all over
        // the system if we do. Instead, fallthrough and return
        // ErrUnsupportedRuntime if we get one.
        fallthrough
    default:
        return swarmapi.ServiceSpec{}, ErrUnsupportedRuntime
    }

    restartPolicy, err := restartPolicyToGRPC(s.TaskTemplate.RestartPolicy)
    if err != nil {
        return swarmapi.ServiceSpec{}, err
    }
    spec.Task.Restart = restartPolicy

    if s.TaskTemplate.Placement != nil {
        var preferences []*swarmapi.PlacementPreference
        for _, pref := range s.TaskTemplate.Placement.Preferences {
            if pref.Spread != nil {
                preferences = append(preferences, &swarmapi.PlacementPreference{
                    Preference: &swarmapi.PlacementPreference_Spread{
                        Spread: &swarmapi.SpreadOver{
                            SpreadDescriptor: pref.Spread.SpreadDescriptor,
                        },
                    },
                })
            }
        }
        var platforms []*swarmapi.Platform
        for _, plat := range s.TaskTemplate.Placement.Platforms {
            platforms = append(platforms, &swarmapi.Platform{
                Architecture: plat.Architecture,
                OS:           plat.OS,
            })
        }
        spec.Task.Placement = &swarmapi.Placement{
            Constraints: s.TaskTemplate.Placement.Constraints,
            Preferences: preferences,
            MaxReplicas: s.TaskTemplate.Placement.MaxReplicas,
            Platforms:   platforms,
        }
    }

    spec.Update, err = updateConfigToGRPC(s.UpdateConfig)
    if err != nil {
        return swarmapi.ServiceSpec{}, err
    }
    spec.Rollback, err = updateConfigToGRPC(s.RollbackConfig)
    if err != nil {
        return swarmapi.ServiceSpec{}, err
    }

    if s.EndpointSpec != nil {
        if s.EndpointSpec.Mode != "" &&
            s.EndpointSpec.Mode != types.ResolutionModeVIP &&
            s.EndpointSpec.Mode != types.ResolutionModeDNSRR {
            return swarmapi.ServiceSpec{}, fmt.Errorf("invalid resolution mode: %q", s.EndpointSpec.Mode)
        }

        spec.Endpoint = &swarmapi.EndpointSpec{}

        spec.Endpoint.Mode = swarmapi.EndpointSpec_ResolutionMode(swarmapi.EndpointSpec_ResolutionMode_value[strings.ToUpper(string(s.EndpointSpec.Mode))])

        for _, portConfig := range s.EndpointSpec.Ports {
            spec.Endpoint.Ports = append(spec.Endpoint.Ports, &swarmapi.PortConfig{
                Name:          portConfig.Name,
                Protocol:      swarmapi.PortConfig_Protocol(swarmapi.PortConfig_Protocol_value[strings.ToUpper(string(portConfig.Protocol))]),
                PublishMode:   swarmapi.PortConfig_PublishMode(swarmapi.PortConfig_PublishMode_value[strings.ToUpper(string(portConfig.PublishMode))]),
                TargetPort:    portConfig.TargetPort,
                PublishedPort: portConfig.PublishedPort,
            })
        }
    }

    // Mode
    numModes := 0
    if s.Mode.Global != nil {
        numModes++
    }
    if s.Mode.Replicated != nil {
        numModes++
    }
    if s.Mode.ReplicatedJob != nil {
        numModes++
    }
    if s.Mode.GlobalJob != nil {
        numModes++
    }

    if numModes > 1 {
        return swarmapi.ServiceSpec{}, fmt.Errorf("must specify only one service mode")
    }

    if s.Mode.Global != nil {
        spec.Mode = &swarmapi.ServiceSpec_Global{
            Global: &swarmapi.GlobalService{},
        }
    } else if s.Mode.GlobalJob != nil {
        spec.Mode = &swarmapi.ServiceSpec_GlobalJob{
            GlobalJob: &swarmapi.GlobalJob{},
        }
    } else if s.Mode.ReplicatedJob != nil {
        // if the service is a replicated job, we have two different kinds of
        // values that might need to be defaulted.

        r := &swarmapi.ReplicatedJob{}
        if s.Mode.ReplicatedJob.MaxConcurrent != nil {
            r.MaxConcurrent = *s.Mode.ReplicatedJob.MaxConcurrent
        } else {
            r.MaxConcurrent = 1
        }

        if s.Mode.ReplicatedJob.TotalCompletions != nil {
            r.TotalCompletions = *s.Mode.ReplicatedJob.TotalCompletions
        } else {
            r.TotalCompletions = r.MaxConcurrent
        }

        spec.Mode = &swarmapi.ServiceSpec_ReplicatedJob{
            ReplicatedJob: r,
        }
    } else if s.Mode.Replicated != nil && s.Mode.Replicated.Replicas != nil {
        spec.Mode = &swarmapi.ServiceSpec_Replicated{
            Replicated: &swarmapi.ReplicatedService{Replicas: *s.Mode.Replicated.Replicas},
        }
    } else {
        spec.Mode = &swarmapi.ServiceSpec_Replicated{
            Replicated: &swarmapi.ReplicatedService{Replicas: 1},
        }
    }

    return spec, nil
}

func annotationsFromGRPC(ann swarmapi.Annotations) types.Annotations {
    a := types.Annotations{
        Name:   ann.Name,
        Labels: ann.Labels,
    }

    if a.Labels == nil {
        a.Labels = make(map[string]string)
    }

    return a
}

// GenericResourcesFromGRPC converts a GRPC GenericResource to a GenericResource
func GenericResourcesFromGRPC(genericRes []*swarmapi.GenericResource) []types.GenericResource {
    var generic []types.GenericResource
    for _, res := range genericRes {
        var current types.GenericResource

        switch r := res.Resource.(type) {
        case *swarmapi.GenericResource_DiscreteResourceSpec:
            current.DiscreteResourceSpec = &types.DiscreteGenericResource{
                Kind:  r.DiscreteResourceSpec.Kind,
                Value: r.DiscreteResourceSpec.Value,
            }
        case *swarmapi.GenericResource_NamedResourceSpec:
            current.NamedResourceSpec = &types.NamedGenericResource{
                Kind:  r.NamedResourceSpec.Kind,
                Value: r.NamedResourceSpec.Value,
            }
        }

        generic = append(generic, current)
    }

    return generic
}

// resourcesFromGRPC creates a ResourceRequirements from the GRPC TaskSpec.
// We currently require the whole TaskSpec to be passed, because PidsLimit
// is returned as part of the container spec, instead of Resources
// TODO move PidsLimit to Resources in the Swarm API
func resourcesFromGRPC(ts *swarmapi.TaskSpec) *types.ResourceRequirements {
    var resources *types.ResourceRequirements

    if cs := ts.GetContainer(); cs != nil && cs.PidsLimit != 0 {
        resources = &types.ResourceRequirements{
            Limits: &types.Limit{
                Pids: cs.PidsLimit,
            },
        }
    }
    if ts.Resources != nil {
        if resources == nil {
            resources = &types.ResourceRequirements{}
        }
        res := ts.Resources
        if res.Limits != nil {
            if resources.Limits == nil {
                resources.Limits = &types.Limit{}
            }
            resources.Limits.NanoCPUs = res.Limits.NanoCPUs
            resources.Limits.MemoryBytes = res.Limits.MemoryBytes
        }
        if res.Reservations != nil {
            resources.Reservations = &types.Resources{
                NanoCPUs:         res.Reservations.NanoCPUs,
                MemoryBytes:      res.Reservations.MemoryBytes,
                GenericResources: GenericResourcesFromGRPC(res.Reservations.Generic),
            }
        }
    }

    return resources
}

// GenericResourcesToGRPC converts a GenericResource to a GRPC GenericResource
func GenericResourcesToGRPC(genericRes []types.GenericResource) []*swarmapi.GenericResource {
    var generic []*swarmapi.GenericResource
    for _, res := range genericRes {
        var r *swarmapi.GenericResource

        if res.DiscreteResourceSpec != nil {
            r = genericresource.NewDiscrete(res.DiscreteResourceSpec.Kind, res.DiscreteResourceSpec.Value)
        } else if res.NamedResourceSpec != nil {
            r = genericresource.NewString(res.NamedResourceSpec.Kind, res.NamedResourceSpec.Value)
        }

        generic = append(generic, r)
    }

    return generic
}

func resourcesToGRPC(res *types.ResourceRequirements) *swarmapi.ResourceRequirements {
    var reqs *swarmapi.ResourceRequirements
    if res != nil {
        reqs = &swarmapi.ResourceRequirements{}
        if res.Limits != nil {
            // TODO add PidsLimit once Swarm API has been updated to move it into Limits
            reqs.Limits = &swarmapi.Resources{
                NanoCPUs:    res.Limits.NanoCPUs,
                MemoryBytes: res.Limits.MemoryBytes,
            }
        }
        if res.Reservations != nil {
            reqs.Reservations = &swarmapi.Resources{
                NanoCPUs:    res.Reservations.NanoCPUs,
                MemoryBytes: res.Reservations.MemoryBytes,
                Generic:     GenericResourcesToGRPC(res.Reservations.GenericResources),
            }
        }
    }
    return reqs
}

func restartPolicyFromGRPC(p *swarmapi.RestartPolicy) *types.RestartPolicy {
    var rp *types.RestartPolicy
    if p != nil {
        rp = &types.RestartPolicy{}

        switch p.Condition {
        case swarmapi.RestartOnNone:
            rp.Condition = types.RestartPolicyConditionNone
        case swarmapi.RestartOnFailure:
            rp.Condition = types.RestartPolicyConditionOnFailure
        case swarmapi.RestartOnAny:
            rp.Condition = types.RestartPolicyConditionAny
        default:
            rp.Condition = types.RestartPolicyConditionAny
        }

        if p.Delay != nil {
            delay, _ := gogotypes.DurationFromProto(p.Delay)
            rp.Delay = &delay
        }
        if p.Window != nil {
            window, _ := gogotypes.DurationFromProto(p.Window)
            rp.Window = &window
        }

        rp.MaxAttempts = &p.MaxAttempts
    }
    return rp
}

func restartPolicyToGRPC(p *types.RestartPolicy) (*swarmapi.RestartPolicy, error) {
    var rp *swarmapi.RestartPolicy
    if p != nil {
        rp = &swarmapi.RestartPolicy{}

        switch p.Condition {
        case types.RestartPolicyConditionNone:
            rp.Condition = swarmapi.RestartOnNone
        case types.RestartPolicyConditionOnFailure:
            rp.Condition = swarmapi.RestartOnFailure
        case types.RestartPolicyConditionAny:
            rp.Condition = swarmapi.RestartOnAny
        default:
            if string(p.Condition) != "" {
                return nil, fmt.Errorf("invalid RestartCondition: %q", p.Condition)
            }
            rp.Condition = swarmapi.RestartOnAny
        }

        if p.Delay != nil {
            rp.Delay = gogotypes.DurationProto(*p.Delay)
        }
        if p.Window != nil {
            rp.Window = gogotypes.DurationProto(*p.Window)
        }
        if p.MaxAttempts != nil {
            rp.MaxAttempts = *p.MaxAttempts
        }
    }
    return rp, nil
}

func placementFromGRPC(p *swarmapi.Placement) *types.Placement {
    if p == nil {
        return nil
    }
    r := &types.Placement{
        Constraints: p.Constraints,
        MaxReplicas: p.MaxReplicas,
    }

    for _, pref := range p.Preferences {
        if spread := pref.GetSpread(); spread != nil {
            r.Preferences = append(r.Preferences, types.PlacementPreference{
                Spread: &types.SpreadOver{
                    SpreadDescriptor: spread.SpreadDescriptor,
                },
            })
        }
    }

    for _, plat := range p.Platforms {
        r.Platforms = append(r.Platforms, types.Platform{
            Architecture: plat.Architecture,
            OS:           plat.OS,
        })
    }

    return r
}

func driverFromGRPC(p *swarmapi.Driver) *types.Driver {
    if p == nil {
        return nil
    }

    return &types.Driver{
        Name:    p.Name,
        Options: p.Options,
    }
}

func driverToGRPC(p *types.Driver) *swarmapi.Driver {
    if p == nil {
        return nil
    }

    return &swarmapi.Driver{
        Name:    p.Name,
        Options: p.Options,
    }
}

func updateConfigFromGRPC(updateConfig *swarmapi.UpdateConfig) *types.UpdateConfig {
    if updateConfig == nil {
        return nil
    }

    converted := &types.UpdateConfig{
        Parallelism:     updateConfig.Parallelism,
        MaxFailureRatio: updateConfig.MaxFailureRatio,
    }

    converted.Delay = updateConfig.Delay
    if updateConfig.Monitor != nil {
        converted.Monitor, _ = gogotypes.DurationFromProto(updateConfig.Monitor)
    }

    switch updateConfig.FailureAction {
    case swarmapi.UpdateConfig_PAUSE:
        converted.FailureAction = types.UpdateFailureActionPause
    case swarmapi.UpdateConfig_CONTINUE:
        converted.FailureAction = types.UpdateFailureActionContinue
    case swarmapi.UpdateConfig_ROLLBACK:
        converted.FailureAction = types.UpdateFailureActionRollback
    }

    switch updateConfig.Order {
    case swarmapi.UpdateConfig_STOP_FIRST:
        converted.Order = types.UpdateOrderStopFirst
    case swarmapi.UpdateConfig_START_FIRST:
        converted.Order = types.UpdateOrderStartFirst
    }

    return converted
}

func updateConfigToGRPC(updateConfig *types.UpdateConfig) (*swarmapi.UpdateConfig, error) {
    if updateConfig == nil {
        return nil, nil
    }

    converted := &swarmapi.UpdateConfig{
        Parallelism:     updateConfig.Parallelism,
        Delay:           updateConfig.Delay,
        MaxFailureRatio: updateConfig.MaxFailureRatio,
    }

    switch updateConfig.FailureAction {
    case types.UpdateFailureActionPause, "":
        converted.FailureAction = swarmapi.UpdateConfig_PAUSE
    case types.UpdateFailureActionContinue:
        converted.FailureAction = swarmapi.UpdateConfig_CONTINUE
    case types.UpdateFailureActionRollback:
        converted.FailureAction = swarmapi.UpdateConfig_ROLLBACK
    default:
        return nil, fmt.Errorf("unrecognized update failure action %s", updateConfig.FailureAction)
    }
    if updateConfig.Monitor != 0 {
        converted.Monitor = gogotypes.DurationProto(updateConfig.Monitor)
    }

    switch updateConfig.Order {
    case types.UpdateOrderStopFirst, "":
        converted.Order = swarmapi.UpdateConfig_STOP_FIRST
    case types.UpdateOrderStartFirst:
        converted.Order = swarmapi.UpdateConfig_START_FIRST
    default:
        return nil, fmt.Errorf("unrecognized update order %s", updateConfig.Order)
    }

    return converted, nil
}

func networkAttachmentSpecFromGRPC(attachment swarmapi.NetworkAttachmentSpec) *types.NetworkAttachmentSpec {
    return &types.NetworkAttachmentSpec{
        ContainerID: attachment.ContainerID,
    }
}

func taskSpecFromGRPC(taskSpec swarmapi.TaskSpec) (types.TaskSpec, error) {
    taskNetworks := make([]types.NetworkAttachmentConfig, 0, len(taskSpec.Networks))
    for _, n := range taskSpec.Networks {
        netConfig := types.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverOpts: n.DriverAttachmentOpts}
        taskNetworks = append(taskNetworks, netConfig)
    }

    t := types.TaskSpec{
        Resources:     resourcesFromGRPC(&taskSpec),
        RestartPolicy: restartPolicyFromGRPC(taskSpec.Restart),
        Placement:     placementFromGRPC(taskSpec.Placement),
        LogDriver:     driverFromGRPC(taskSpec.LogDriver),
        Networks:      taskNetworks,
        ForceUpdate:   taskSpec.ForceUpdate,
    }

    switch taskSpec.GetRuntime().(type) {
    case *swarmapi.TaskSpec_Container, nil:
        c := taskSpec.GetContainer()
        if c != nil {
            t.ContainerSpec = containerSpecFromGRPC(c)
        }
    case *swarmapi.TaskSpec_Generic:
        g := taskSpec.GetGeneric()
        if g != nil {
            switch g.Kind {
            case string(types.RuntimePlugin):
                var p runtime.PluginSpec
                if err := proto.Unmarshal(g.Payload.Value, &p); err != nil {
                    return t, errors.Wrap(err, "error unmarshalling plugin spec")
                }
                t.PluginSpec = &p
            }
        }
    case *swarmapi.TaskSpec_Attachment:
        a := taskSpec.GetAttachment()
        if a != nil {
            t.NetworkAttachmentSpec = networkAttachmentSpecFromGRPC(*a)
        }
        t.Runtime = types.RuntimeNetworkAttachment
    }

    return t, nil
}