diff --git a/api/swagger.yaml b/api/swagger.yaml index 459ce9b186..c0b442416d 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2688,6 +2688,13 @@ definitions: - "default" - "process" - "hyperv" + NetworkAttachmentSpec: + description: "Read-only spec type for non-swarm containers attached to swarm overlay networks" + type: "object" + properties: + ContainerID: + description: "ID of the container represented by this task" + type: "string" Resources: description: "Resource requirements which apply to each individual container created as part of the service." type: "object" diff --git a/api/types/swarm/runtime.go b/api/types/swarm/runtime.go index 81b5f4cfd9..0c77403ccf 100644 --- a/api/types/swarm/runtime.go +++ b/api/types/swarm/runtime.go @@ -11,9 +11,17 @@ const ( RuntimeContainer RuntimeType = "container" // RuntimePlugin is the plugin based runtime RuntimePlugin RuntimeType = "plugin" + // RuntimeNetworkAttachment is the network attachment runtime + RuntimeNetworkAttachment RuntimeType = "attachment" // RuntimeURLContainer is the proto url for the container type RuntimeURLContainer RuntimeURL = "types.docker.com/RuntimeContainer" // RuntimeURLPlugin is the proto url for the plugin type RuntimeURLPlugin RuntimeURL = "types.docker.com/RuntimePlugin" ) + +// NetworkAttachmentSpec represents the runtime spec type for network +// attachment tasks +type NetworkAttachmentSpec struct { + ContainerID string +} diff --git a/api/types/swarm/task.go b/api/types/swarm/task.go index 5c2bc492e4..b35605d12f 100644 --- a/api/types/swarm/task.go +++ b/api/types/swarm/task.go @@ -60,10 +60,13 @@ type Task struct { // TaskSpec represents the spec of a task. type TaskSpec struct { - // ContainerSpec and PluginSpec are mutually exclusive. - // PluginSpec will only be used when the `Runtime` field is set to `plugin` - ContainerSpec *ContainerSpec `json:",omitempty"` - PluginSpec *runtime.PluginSpec `json:",omitempty"` + // ContainerSpec, NetworkAttachmentSpec, and PluginSpec are mutually exclusive. + // PluginSpec is only used when the `Runtime` field is set to `plugin` + // NetworkAttachmentSpec is used if the `Runtime` field is set to + // `attachment`. + ContainerSpec *ContainerSpec `json:",omitempty"` + PluginSpec *runtime.PluginSpec `json:",omitempty"` + NetworkAttachmentSpec *NetworkAttachmentSpec `json:",omitempty"` Resources *ResourceRequirements `json:",omitempty"` RestartPolicy *RestartPolicy `json:",omitempty"` diff --git a/daemon/cluster/convert/service.go b/daemon/cluster/convert/service.go index 7aa34fa77f..5a1609aa01 100644 --- a/daemon/cluster/convert/service.go +++ b/daemon/cluster/convert/service.go @@ -17,6 +17,8 @@ import ( 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. @@ -176,15 +178,18 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) { return swarmapi.ServiceSpec{}, err } 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.Mode.Replicated != nil { - return swarmapi.ServiceSpec{}, errors.New("plugins must not use replicated mode") - } - - s.Mode.Global = &types.GlobalService{} // must always be global - 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 @@ -198,7 +203,16 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) { }, }, } + } else { + return swarmapi.ServiceSpec{}, ErrMismatchedRuntime } + case types.RuntimeNetworkAttachment: + // NOTE(dperny) I'm leaving this case here for completeness. The actual + // code is left out 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 } @@ -573,6 +587,12 @@ func updateConfigToGRPC(updateConfig *types.UpdateConfig) (*swarmapi.UpdateConfi 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 { @@ -607,6 +627,12 @@ func taskSpecFromGRPC(taskSpec swarmapi.TaskSpec) (types.TaskSpec, error) { t.PluginSpec = &p } } + case *swarmapi.TaskSpec_Attachment: + a := taskSpec.GetAttachment() + if a != nil { + t.NetworkAttachmentSpec = networkAttachmentSpecFromGRPC(*a) + } + t.Runtime = types.RuntimeNetworkAttachment } return t, nil diff --git a/daemon/cluster/convert/service_test.go b/daemon/cluster/convert/service_test.go index 0794af99a6..826cf6fbef 100644 --- a/daemon/cluster/convert/service_test.go +++ b/daemon/cluster/convert/service_test.go @@ -232,3 +232,77 @@ func TestServiceConvertFromGRPCIsolation(t *testing.T) { }) } } + +func TestServiceConvertToGRPCNetworkAtachmentRuntime(t *testing.T) { + someid := "asfjkl" + s := swarmtypes.ServiceSpec{ + TaskTemplate: swarmtypes.TaskSpec{ + Runtime: swarmtypes.RuntimeNetworkAttachment, + NetworkAttachmentSpec: &swarmtypes.NetworkAttachmentSpec{ + ContainerID: someid, + }, + }, + } + + // discard the service, which will be empty + _, err := ServiceSpecToGRPC(s) + if err == nil { + t.Fatalf("expected error %v but got no error", ErrUnsupportedRuntime) + } + if err != ErrUnsupportedRuntime { + t.Fatalf("expected error %v but got error %v", ErrUnsupportedRuntime, err) + } +} + +func TestServiceConvertToGRPCMismatchedRuntime(t *testing.T) { + // NOTE(dperny): an earlier version of this test was for code that also + // converted network attachment tasks to GRPC. that conversion code was + // removed, so if this loop body seems a bit complicated, that's why. + for i, rt := range []swarmtypes.RuntimeType{ + swarmtypes.RuntimeContainer, + swarmtypes.RuntimePlugin, + } { + for j, spec := range []swarmtypes.TaskSpec{ + {ContainerSpec: &swarmtypes.ContainerSpec{}}, + {PluginSpec: &runtime.PluginSpec{}}, + } { + // skip the cases, where the indices match, which would not error + if i == j { + continue + } + // set the task spec, then change the runtime + s := swarmtypes.ServiceSpec{ + TaskTemplate: spec, + } + s.TaskTemplate.Runtime = rt + + if _, err := ServiceSpecToGRPC(s); err != ErrMismatchedRuntime { + t.Fatalf("expected %v got %v", ErrMismatchedRuntime, err) + } + } + } +} + +func TestTaskConvertFromGRPCNetworkAttachment(t *testing.T) { + containerID := "asdfjkl" + s := swarmapi.TaskSpec{ + Runtime: &swarmapi.TaskSpec_Attachment{ + Attachment: &swarmapi.NetworkAttachmentSpec{ + ContainerID: containerID, + }, + }, + } + ts, err := taskSpecFromGRPC(s) + if err != nil { + t.Fatal(err) + } + if ts.NetworkAttachmentSpec == nil { + t.Fatal("expected task spec to have network attachment spec") + } + if ts.NetworkAttachmentSpec.ContainerID != containerID { + t.Fatalf("expected network attachment spec container id to be %q, was %q", containerID, ts.NetworkAttachmentSpec.ContainerID) + } + if ts.Runtime != swarmtypes.RuntimeNetworkAttachment { + t.Fatalf("expected Runtime to be %v", swarmtypes.RuntimeNetworkAttachment) + } +} diff --git a/daemon/cluster/convert/task.go b/daemon/cluster/convert/task.go index dbe6d7414d..72e2805e1e 100644 --- a/daemon/cluster/convert/task.go +++ b/daemon/cluster/convert/task.go @@ -10,9 +10,6 @@ import ( // TaskFromGRPC converts a grpc Task to a Task. func TaskFromGRPC(t swarmapi.Task) (types.Task, error) { - if t.Spec.GetAttachment() != nil { - return types.Task{}, nil - } containerStatus := t.Status.GetContainer() taskSpec, err := taskSpecFromGRPC(t.Spec) if err != nil { diff --git a/daemon/cluster/services.go b/daemon/cluster/services.go index ed438e93a0..c14037645c 100644 --- a/daemon/cluster/services.go +++ b/daemon/cluster/services.go @@ -135,6 +135,8 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string, queryRe resp = &apitypes.ServiceCreateResponse{} switch serviceSpec.Task.Runtime.(type) { + case *swarmapi.TaskSpec_Attachment: + return fmt.Errorf("invalid task spec: spec type %q not supported", types.RuntimeNetworkAttachment) // handle other runtimes here case *swarmapi.TaskSpec_Generic: switch serviceSpec.Task.GetGeneric().Kind { @@ -244,6 +246,8 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ resp = &apitypes.ServiceUpdateResponse{} switch serviceSpec.Task.Runtime.(type) { + case *swarmapi.TaskSpec_Attachment: + return fmt.Errorf("invalid task spec: spec type %q not supported", types.RuntimeNetworkAttachment) case *swarmapi.TaskSpec_Generic: switch serviceSpec.Task.GetGeneric().Kind { case string(types.RuntimePlugin):