Browse Source

Output network attachment task information

Adds functionality to parse and return network attachment spec
information. Network attachment tasks are phony tasks created in
swarmkit to deal with unmanaged containers attached to swarmkit. Before
this change, attempting `docker inspect` on the task id of a network
attachment task would result in an empty task object. After this change,
a full task object is returned

Fixes #26548 the correct way.

Signed-off-by: Drew Erny <drew.erny@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Drew Erny 7 years ago
parent
commit
5b69ff466e

+ 7 - 0
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"

+ 8 - 0
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
+}

+ 7 - 4
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"`

+ 32 - 6
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")
-		}
+		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
+			s.Mode.Global = &types.GlobalService{} // must always be global
 
-		if s.TaskTemplate.PluginSpec != nil {
 			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

+ 74 - 0
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)
+	}
+}

+ 0 - 3
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 {

+ 4 - 0
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):