github.com/moby/docker@v26.1.3+incompatible/daemon/cluster/convert/service_test.go (about)

     1  package convert // import "github.com/docker/docker/daemon/cluster/convert"
     2  
     3  import (
     4  	"testing"
     5  
     6  	containertypes "github.com/docker/docker/api/types/container"
     7  	"github.com/docker/docker/api/types/mount"
     8  	swarmtypes "github.com/docker/docker/api/types/swarm"
     9  	"github.com/docker/docker/api/types/swarm/runtime"
    10  	google_protobuf3 "github.com/gogo/protobuf/types"
    11  	swarmapi "github.com/moby/swarmkit/v2/api"
    12  	"gotest.tools/v3/assert"
    13  	is "gotest.tools/v3/assert/cmp"
    14  )
    15  
    16  func TestServiceConvertFromGRPCRuntimeContainer(t *testing.T) {
    17  	gs := swarmapi.Service{
    18  		Meta: swarmapi.Meta{
    19  			Version: swarmapi.Version{
    20  				Index: 1,
    21  			},
    22  			CreatedAt: nil,
    23  			UpdatedAt: nil,
    24  		},
    25  		SpecVersion: &swarmapi.Version{
    26  			Index: 1,
    27  		},
    28  		Spec: swarmapi.ServiceSpec{
    29  			Task: swarmapi.TaskSpec{
    30  				Runtime: &swarmapi.TaskSpec_Container{
    31  					Container: &swarmapi.ContainerSpec{
    32  						Image: "alpine:latest",
    33  					},
    34  				},
    35  			},
    36  		},
    37  	}
    38  
    39  	svc, err := ServiceFromGRPC(gs)
    40  	if err != nil {
    41  		t.Fatal(err)
    42  	}
    43  
    44  	if svc.Spec.TaskTemplate.Runtime != swarmtypes.RuntimeContainer {
    45  		t.Fatalf("expected type %s; received %T", swarmtypes.RuntimeContainer, svc.Spec.TaskTemplate.Runtime)
    46  	}
    47  }
    48  
    49  func TestServiceConvertFromGRPCGenericRuntimePlugin(t *testing.T) {
    50  	kind := string(swarmtypes.RuntimePlugin)
    51  	url := swarmtypes.RuntimeURLPlugin
    52  	gs := swarmapi.Service{
    53  		Meta: swarmapi.Meta{
    54  			Version: swarmapi.Version{
    55  				Index: 1,
    56  			},
    57  			CreatedAt: nil,
    58  			UpdatedAt: nil,
    59  		},
    60  		SpecVersion: &swarmapi.Version{
    61  			Index: 1,
    62  		},
    63  		Spec: swarmapi.ServiceSpec{
    64  			Task: swarmapi.TaskSpec{
    65  				Runtime: &swarmapi.TaskSpec_Generic{
    66  					Generic: &swarmapi.GenericRuntimeSpec{
    67  						Kind: kind,
    68  						Payload: &google_protobuf3.Any{
    69  							TypeUrl: string(url),
    70  						},
    71  					},
    72  				},
    73  			},
    74  		},
    75  	}
    76  
    77  	svc, err := ServiceFromGRPC(gs)
    78  	if err != nil {
    79  		t.Fatal(err)
    80  	}
    81  
    82  	if svc.Spec.TaskTemplate.Runtime != swarmtypes.RuntimePlugin {
    83  		t.Fatalf("expected type %s; received %T", swarmtypes.RuntimePlugin, svc.Spec.TaskTemplate.Runtime)
    84  	}
    85  }
    86  
    87  func TestServiceConvertToGRPCGenericRuntimePlugin(t *testing.T) {
    88  	s := swarmtypes.ServiceSpec{
    89  		TaskTemplate: swarmtypes.TaskSpec{
    90  			Runtime:    swarmtypes.RuntimePlugin,
    91  			PluginSpec: &runtime.PluginSpec{},
    92  		},
    93  		Mode: swarmtypes.ServiceMode{
    94  			Global: &swarmtypes.GlobalService{},
    95  		},
    96  	}
    97  
    98  	svc, err := ServiceSpecToGRPC(s)
    99  	if err != nil {
   100  		t.Fatal(err)
   101  	}
   102  
   103  	v, ok := svc.Task.Runtime.(*swarmapi.TaskSpec_Generic)
   104  	if !ok {
   105  		t.Fatal("expected type swarmapi.TaskSpec_Generic")
   106  	}
   107  
   108  	if v.Generic.Payload.TypeUrl != string(swarmtypes.RuntimeURLPlugin) {
   109  		t.Fatalf("expected url %s; received %s", swarmtypes.RuntimeURLPlugin, v.Generic.Payload.TypeUrl)
   110  	}
   111  }
   112  
   113  func TestServiceConvertToGRPCContainerRuntime(t *testing.T) {
   114  	const imgName = "alpine:latest"
   115  	s := swarmtypes.ServiceSpec{
   116  		TaskTemplate: swarmtypes.TaskSpec{
   117  			ContainerSpec: &swarmtypes.ContainerSpec{
   118  				Image: imgName,
   119  			},
   120  		},
   121  		Mode: swarmtypes.ServiceMode{
   122  			Global: &swarmtypes.GlobalService{},
   123  		},
   124  	}
   125  
   126  	svc, err := ServiceSpecToGRPC(s)
   127  	if err != nil {
   128  		t.Fatal(err)
   129  	}
   130  
   131  	v, ok := svc.Task.Runtime.(*swarmapi.TaskSpec_Container)
   132  	if !ok {
   133  		t.Fatal("expected type swarmapi.TaskSpec_Container")
   134  	}
   135  
   136  	if v.Container.Image != imgName {
   137  		t.Fatalf("expected image %s; received %s", imgName, v.Container.Image)
   138  	}
   139  }
   140  
   141  func TestServiceConvertToGRPCGenericRuntimeCustom(t *testing.T) {
   142  	s := swarmtypes.ServiceSpec{
   143  		TaskTemplate: swarmtypes.TaskSpec{
   144  			Runtime: "customruntime",
   145  		},
   146  		Mode: swarmtypes.ServiceMode{
   147  			Global: &swarmtypes.GlobalService{},
   148  		},
   149  	}
   150  
   151  	if _, err := ServiceSpecToGRPC(s); err != ErrUnsupportedRuntime {
   152  		t.Fatal(err)
   153  	}
   154  }
   155  
   156  func TestServiceConvertToGRPCIsolation(t *testing.T) {
   157  	cases := []struct {
   158  		name string
   159  		from containertypes.Isolation
   160  		to   swarmapi.ContainerSpec_Isolation
   161  	}{
   162  		{name: "empty", from: containertypes.IsolationEmpty, to: swarmapi.ContainerIsolationDefault},
   163  		{name: "default", from: containertypes.IsolationDefault, to: swarmapi.ContainerIsolationDefault},
   164  		{name: "process", from: containertypes.IsolationProcess, to: swarmapi.ContainerIsolationProcess},
   165  		{name: "hyperv", from: containertypes.IsolationHyperV, to: swarmapi.ContainerIsolationHyperV},
   166  		{name: "proCess", from: containertypes.Isolation("proCess"), to: swarmapi.ContainerIsolationProcess},
   167  		{name: "hypErv", from: containertypes.Isolation("hypErv"), to: swarmapi.ContainerIsolationHyperV},
   168  	}
   169  	for _, c := range cases {
   170  		t.Run(c.name, func(t *testing.T) {
   171  			s := swarmtypes.ServiceSpec{
   172  				TaskTemplate: swarmtypes.TaskSpec{
   173  					ContainerSpec: &swarmtypes.ContainerSpec{
   174  						Image:     "alpine:latest",
   175  						Isolation: c.from,
   176  					},
   177  				},
   178  				Mode: swarmtypes.ServiceMode{
   179  					Global: &swarmtypes.GlobalService{},
   180  				},
   181  			}
   182  			res, err := ServiceSpecToGRPC(s)
   183  			assert.NilError(t, err)
   184  			v, ok := res.Task.Runtime.(*swarmapi.TaskSpec_Container)
   185  			if !ok {
   186  				t.Fatal("expected type swarmapi.TaskSpec_Container")
   187  			}
   188  			assert.Equal(t, c.to, v.Container.Isolation)
   189  		})
   190  	}
   191  }
   192  
   193  func TestServiceConvertFromGRPCIsolation(t *testing.T) {
   194  	cases := []struct {
   195  		name string
   196  		from swarmapi.ContainerSpec_Isolation
   197  		to   containertypes.Isolation
   198  	}{
   199  		{name: "default", to: containertypes.IsolationDefault, from: swarmapi.ContainerIsolationDefault},
   200  		{name: "process", to: containertypes.IsolationProcess, from: swarmapi.ContainerIsolationProcess},
   201  		{name: "hyperv", to: containertypes.IsolationHyperV, from: swarmapi.ContainerIsolationHyperV},
   202  	}
   203  	for _, c := range cases {
   204  		t.Run(c.name, func(t *testing.T) {
   205  			gs := swarmapi.Service{
   206  				Meta: swarmapi.Meta{
   207  					Version: swarmapi.Version{
   208  						Index: 1,
   209  					},
   210  					CreatedAt: nil,
   211  					UpdatedAt: nil,
   212  				},
   213  				SpecVersion: &swarmapi.Version{
   214  					Index: 1,
   215  				},
   216  				Spec: swarmapi.ServiceSpec{
   217  					Task: swarmapi.TaskSpec{
   218  						Runtime: &swarmapi.TaskSpec_Container{
   219  							Container: &swarmapi.ContainerSpec{
   220  								Image:     "alpine:latest",
   221  								Isolation: c.from,
   222  							},
   223  						},
   224  					},
   225  				},
   226  			}
   227  
   228  			svc, err := ServiceFromGRPC(gs)
   229  			if err != nil {
   230  				t.Fatal(err)
   231  			}
   232  
   233  			assert.Equal(t, c.to, svc.Spec.TaskTemplate.ContainerSpec.Isolation)
   234  		})
   235  	}
   236  }
   237  
   238  func TestServiceConvertToGRPCCredentialSpec(t *testing.T) {
   239  	cases := []struct {
   240  		name        string
   241  		from        swarmtypes.CredentialSpec
   242  		to          swarmapi.Privileges_CredentialSpec
   243  		expectedErr string
   244  	}{
   245  		{
   246  			name:        "empty credential spec",
   247  			from:        swarmtypes.CredentialSpec{},
   248  			to:          swarmapi.Privileges_CredentialSpec{},
   249  			expectedErr: `invalid CredentialSpec: must either provide "file", "registry", or "config" for credential spec`,
   250  		},
   251  		{
   252  			name: "config and file credential spec",
   253  			from: swarmtypes.CredentialSpec{
   254  				Config: "0bt9dmxjvjiqermk6xrop3ekq",
   255  				File:   "spec.json",
   256  			},
   257  			to:          swarmapi.Privileges_CredentialSpec{},
   258  			expectedErr: `invalid CredentialSpec: cannot specify both "config" and "file" credential specs`,
   259  		},
   260  		{
   261  			name: "config and registry credential spec",
   262  			from: swarmtypes.CredentialSpec{
   263  				Config:   "0bt9dmxjvjiqermk6xrop3ekq",
   264  				Registry: "testing",
   265  			},
   266  			to:          swarmapi.Privileges_CredentialSpec{},
   267  			expectedErr: `invalid CredentialSpec: cannot specify both "config" and "registry" credential specs`,
   268  		},
   269  		{
   270  			name: "file and registry credential spec",
   271  			from: swarmtypes.CredentialSpec{
   272  				File:     "spec.json",
   273  				Registry: "testing",
   274  			},
   275  			to:          swarmapi.Privileges_CredentialSpec{},
   276  			expectedErr: `invalid CredentialSpec: cannot specify both "file" and "registry" credential specs`,
   277  		},
   278  		{
   279  			name: "config and file and registry credential spec",
   280  			from: swarmtypes.CredentialSpec{
   281  				Config:   "0bt9dmxjvjiqermk6xrop3ekq",
   282  				File:     "spec.json",
   283  				Registry: "testing",
   284  			},
   285  			to:          swarmapi.Privileges_CredentialSpec{},
   286  			expectedErr: `invalid CredentialSpec: cannot specify both "config", "file", and "registry" credential specs`,
   287  		},
   288  		{
   289  			name: "config credential spec",
   290  			from: swarmtypes.CredentialSpec{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
   291  			to: swarmapi.Privileges_CredentialSpec{
   292  				Source: &swarmapi.Privileges_CredentialSpec_Config{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
   293  			},
   294  		},
   295  		{
   296  			name: "file credential spec",
   297  			from: swarmtypes.CredentialSpec{File: "foo.json"},
   298  			to: swarmapi.Privileges_CredentialSpec{
   299  				Source: &swarmapi.Privileges_CredentialSpec_File{File: "foo.json"},
   300  			},
   301  		},
   302  		{
   303  			name: "registry credential spec",
   304  			from: swarmtypes.CredentialSpec{Registry: "testing"},
   305  			to: swarmapi.Privileges_CredentialSpec{
   306  				Source: &swarmapi.Privileges_CredentialSpec_Registry{Registry: "testing"},
   307  			},
   308  		},
   309  	}
   310  
   311  	for _, c := range cases {
   312  		c := c
   313  		t.Run(c.name, func(t *testing.T) {
   314  			s := swarmtypes.ServiceSpec{
   315  				TaskTemplate: swarmtypes.TaskSpec{
   316  					ContainerSpec: &swarmtypes.ContainerSpec{
   317  						Privileges: &swarmtypes.Privileges{
   318  							CredentialSpec: &c.from,
   319  						},
   320  					},
   321  				},
   322  			}
   323  
   324  			res, err := ServiceSpecToGRPC(s)
   325  			if c.expectedErr != "" {
   326  				assert.Error(t, err, c.expectedErr)
   327  				return
   328  			}
   329  
   330  			assert.NilError(t, err)
   331  			v, ok := res.Task.Runtime.(*swarmapi.TaskSpec_Container)
   332  			if !ok {
   333  				t.Fatal("expected type swarmapi.TaskSpec_Container")
   334  			}
   335  			assert.DeepEqual(t, c.to, *v.Container.Privileges.CredentialSpec)
   336  		})
   337  	}
   338  }
   339  
   340  func TestServiceConvertFromGRPCCredentialSpec(t *testing.T) {
   341  	cases := []struct {
   342  		name string
   343  		from swarmapi.Privileges_CredentialSpec
   344  		to   *swarmtypes.CredentialSpec
   345  	}{
   346  		{
   347  			name: "empty credential spec",
   348  			from: swarmapi.Privileges_CredentialSpec{},
   349  			to:   &swarmtypes.CredentialSpec{},
   350  		},
   351  		{
   352  			name: "config credential spec",
   353  			from: swarmapi.Privileges_CredentialSpec{
   354  				Source: &swarmapi.Privileges_CredentialSpec_Config{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
   355  			},
   356  			to: &swarmtypes.CredentialSpec{Config: "0bt9dmxjvjiqermk6xrop3ekq"},
   357  		},
   358  		{
   359  			name: "file credential spec",
   360  			from: swarmapi.Privileges_CredentialSpec{
   361  				Source: &swarmapi.Privileges_CredentialSpec_File{File: "foo.json"},
   362  			},
   363  			to: &swarmtypes.CredentialSpec{File: "foo.json"},
   364  		},
   365  		{
   366  			name: "registry credential spec",
   367  			from: swarmapi.Privileges_CredentialSpec{
   368  				Source: &swarmapi.Privileges_CredentialSpec_Registry{Registry: "testing"},
   369  			},
   370  			to: &swarmtypes.CredentialSpec{Registry: "testing"},
   371  		},
   372  	}
   373  
   374  	for _, tc := range cases {
   375  		tc := tc
   376  
   377  		t.Run(tc.name, func(t *testing.T) {
   378  			gs := swarmapi.Service{
   379  				Spec: swarmapi.ServiceSpec{
   380  					Task: swarmapi.TaskSpec{
   381  						Runtime: &swarmapi.TaskSpec_Container{
   382  							Container: &swarmapi.ContainerSpec{
   383  								Privileges: &swarmapi.Privileges{
   384  									CredentialSpec: &tc.from,
   385  								},
   386  							},
   387  						},
   388  					},
   389  				},
   390  			}
   391  
   392  			svc, err := ServiceFromGRPC(gs)
   393  			assert.NilError(t, err)
   394  			assert.DeepEqual(t, svc.Spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec, tc.to)
   395  		})
   396  	}
   397  }
   398  
   399  func TestServiceConvertToGRPCNetworkAtachmentRuntime(t *testing.T) {
   400  	someid := "asfjkl"
   401  	s := swarmtypes.ServiceSpec{
   402  		TaskTemplate: swarmtypes.TaskSpec{
   403  			Runtime: swarmtypes.RuntimeNetworkAttachment,
   404  			NetworkAttachmentSpec: &swarmtypes.NetworkAttachmentSpec{
   405  				ContainerID: someid,
   406  			},
   407  		},
   408  	}
   409  
   410  	// discard the service, which will be empty
   411  	_, err := ServiceSpecToGRPC(s)
   412  	if err == nil {
   413  		t.Fatalf("expected error %v but got no error", ErrUnsupportedRuntime)
   414  	}
   415  	if err != ErrUnsupportedRuntime {
   416  		t.Fatalf("expected error %v but got error %v", ErrUnsupportedRuntime, err)
   417  	}
   418  }
   419  
   420  func TestServiceConvertToGRPCMismatchedRuntime(t *testing.T) {
   421  	// NOTE(dperny): an earlier version of this test was for code that also
   422  	// converted network attachment tasks to GRPC. that conversion code was
   423  	// removed, so if this loop body seems a bit complicated, that's why.
   424  	for i, rt := range []swarmtypes.RuntimeType{
   425  		swarmtypes.RuntimeContainer,
   426  		swarmtypes.RuntimePlugin,
   427  	} {
   428  		for j, spec := range []swarmtypes.TaskSpec{
   429  			{ContainerSpec: &swarmtypes.ContainerSpec{}},
   430  			{PluginSpec: &runtime.PluginSpec{}},
   431  		} {
   432  			// skip the cases, where the indices match, which would not error
   433  			if i == j {
   434  				continue
   435  			}
   436  			// set the task spec, then change the runtime
   437  			s := swarmtypes.ServiceSpec{
   438  				TaskTemplate: spec,
   439  			}
   440  			s.TaskTemplate.Runtime = rt
   441  
   442  			if _, err := ServiceSpecToGRPC(s); err != ErrMismatchedRuntime {
   443  				t.Fatalf("expected %v got %v", ErrMismatchedRuntime, err)
   444  			}
   445  		}
   446  	}
   447  }
   448  
   449  func TestTaskConvertFromGRPCNetworkAttachment(t *testing.T) {
   450  	containerID := "asdfjkl"
   451  	s := swarmapi.TaskSpec{
   452  		Runtime: &swarmapi.TaskSpec_Attachment{
   453  			Attachment: &swarmapi.NetworkAttachmentSpec{
   454  				ContainerID: containerID,
   455  			},
   456  		},
   457  	}
   458  	ts, err := taskSpecFromGRPC(s)
   459  	if err != nil {
   460  		t.Fatal(err)
   461  	}
   462  	if ts.NetworkAttachmentSpec == nil {
   463  		t.Fatal("expected task spec to have network attachment spec")
   464  	}
   465  	if ts.NetworkAttachmentSpec.ContainerID != containerID {
   466  		t.Fatalf("expected network attachment spec container id to be %q, was %q", containerID, ts.NetworkAttachmentSpec.ContainerID)
   467  	}
   468  	if ts.Runtime != swarmtypes.RuntimeNetworkAttachment {
   469  		t.Fatalf("expected Runtime to be %v", swarmtypes.RuntimeNetworkAttachment)
   470  	}
   471  }
   472  
   473  // TestServiceConvertFromGRPCConfigs tests that converting config references
   474  // from GRPC is correct
   475  func TestServiceConvertFromGRPCConfigs(t *testing.T) {
   476  	cases := []struct {
   477  		name string
   478  		from *swarmapi.ConfigReference
   479  		to   *swarmtypes.ConfigReference
   480  	}{
   481  		{
   482  			name: "file",
   483  			from: &swarmapi.ConfigReference{
   484  				ConfigID:   "configFile",
   485  				ConfigName: "configFile",
   486  				Target: &swarmapi.ConfigReference_File{
   487  					// skip mode, if everything else here works mode will too. otherwise we'd need to import os.
   488  					File: &swarmapi.FileTarget{Name: "foo", UID: "bar", GID: "baz"},
   489  				},
   490  			},
   491  			to: &swarmtypes.ConfigReference{
   492  				ConfigID:   "configFile",
   493  				ConfigName: "configFile",
   494  				File:       &swarmtypes.ConfigReferenceFileTarget{Name: "foo", UID: "bar", GID: "baz"},
   495  			},
   496  		},
   497  		{
   498  			name: "runtime",
   499  			from: &swarmapi.ConfigReference{
   500  				ConfigID:   "configRuntime",
   501  				ConfigName: "configRuntime",
   502  				Target:     &swarmapi.ConfigReference_Runtime{Runtime: &swarmapi.RuntimeTarget{}},
   503  			},
   504  			to: &swarmtypes.ConfigReference{
   505  				ConfigID:   "configRuntime",
   506  				ConfigName: "configRuntime",
   507  				Runtime:    &swarmtypes.ConfigReferenceRuntimeTarget{},
   508  			},
   509  		},
   510  	}
   511  
   512  	for _, tc := range cases {
   513  		t.Run(tc.name, func(t *testing.T) {
   514  			grpcService := swarmapi.Service{
   515  				Spec: swarmapi.ServiceSpec{
   516  					Task: swarmapi.TaskSpec{
   517  						Runtime: &swarmapi.TaskSpec_Container{
   518  							Container: &swarmapi.ContainerSpec{
   519  								Configs: []*swarmapi.ConfigReference{tc.from},
   520  							},
   521  						},
   522  					},
   523  				},
   524  			}
   525  
   526  			engineService, err := ServiceFromGRPC(grpcService)
   527  			assert.NilError(t, err)
   528  			assert.DeepEqual(t,
   529  				engineService.Spec.TaskTemplate.ContainerSpec.Configs[0],
   530  				tc.to,
   531  			)
   532  		})
   533  	}
   534  }
   535  
   536  // TestServiceConvertToGRPCConfigs tests that converting config references to
   537  // GRPC is correct
   538  func TestServiceConvertToGRPCConfigs(t *testing.T) {
   539  	cases := []struct {
   540  		name        string
   541  		from        *swarmtypes.ConfigReference
   542  		to          *swarmapi.ConfigReference
   543  		expectedErr string
   544  	}{
   545  		{
   546  			name: "file",
   547  			from: &swarmtypes.ConfigReference{
   548  				ConfigID:   "configFile",
   549  				ConfigName: "configFile",
   550  				File:       &swarmtypes.ConfigReferenceFileTarget{Name: "foo", UID: "bar", GID: "baz"},
   551  			},
   552  			to: &swarmapi.ConfigReference{
   553  				ConfigID:   "configFile",
   554  				ConfigName: "configFile",
   555  				Target: &swarmapi.ConfigReference_File{
   556  					// skip mode, if everything else here works mode will too. otherwise we'd need to import os.
   557  					File: &swarmapi.FileTarget{Name: "foo", UID: "bar", GID: "baz"},
   558  				},
   559  			},
   560  		},
   561  		{
   562  			name: "runtime",
   563  			from: &swarmtypes.ConfigReference{
   564  				ConfigID:   "configRuntime",
   565  				ConfigName: "configRuntime",
   566  				Runtime:    &swarmtypes.ConfigReferenceRuntimeTarget{},
   567  			},
   568  			to: &swarmapi.ConfigReference{
   569  				ConfigID:   "configRuntime",
   570  				ConfigName: "configRuntime",
   571  				Target:     &swarmapi.ConfigReference_Runtime{Runtime: &swarmapi.RuntimeTarget{}},
   572  			},
   573  		},
   574  		{
   575  			name: "file and runtime",
   576  			from: &swarmtypes.ConfigReference{
   577  				ConfigID:   "fileAndRuntime",
   578  				ConfigName: "fileAndRuntime",
   579  				File:       &swarmtypes.ConfigReferenceFileTarget{},
   580  				Runtime:    &swarmtypes.ConfigReferenceRuntimeTarget{},
   581  			},
   582  			expectedErr: "invalid Config: cannot specify both File and Runtime",
   583  		},
   584  		{
   585  			name: "none",
   586  			from: &swarmtypes.ConfigReference{
   587  				ConfigID:   "none",
   588  				ConfigName: "none",
   589  			},
   590  			expectedErr: "invalid Config: either File or Runtime should be set",
   591  		},
   592  	}
   593  
   594  	for _, tc := range cases {
   595  		t.Run(tc.name, func(t *testing.T) {
   596  			engineServiceSpec := swarmtypes.ServiceSpec{
   597  				TaskTemplate: swarmtypes.TaskSpec{
   598  					ContainerSpec: &swarmtypes.ContainerSpec{
   599  						Configs: []*swarmtypes.ConfigReference{tc.from},
   600  					},
   601  				},
   602  			}
   603  
   604  			grpcServiceSpec, err := ServiceSpecToGRPC(engineServiceSpec)
   605  			if tc.expectedErr != "" {
   606  				assert.Error(t, err, tc.expectedErr)
   607  				return
   608  			}
   609  
   610  			assert.NilError(t, err)
   611  			taskRuntime := grpcServiceSpec.Task.Runtime.(*swarmapi.TaskSpec_Container)
   612  			assert.DeepEqual(t, taskRuntime.Container.Configs[0], tc.to)
   613  		})
   614  	}
   615  }
   616  
   617  func TestServiceConvertToGRPCVolumeSubpath(t *testing.T) {
   618  	s := swarmtypes.ServiceSpec{
   619  		TaskTemplate: swarmtypes.TaskSpec{
   620  			ContainerSpec: &swarmtypes.ContainerSpec{
   621  				Mounts: []mount.Mount{
   622  					{
   623  						Source:   "/foo/bar",
   624  						Target:   "/baz",
   625  						Type:     mount.TypeVolume,
   626  						ReadOnly: false,
   627  						VolumeOptions: &mount.VolumeOptions{
   628  							Subpath: "sub",
   629  						},
   630  					},
   631  				},
   632  			},
   633  		},
   634  	}
   635  
   636  	g, err := ServiceSpecToGRPC(s)
   637  	assert.NilError(t, err)
   638  
   639  	v, ok := g.Task.Runtime.(*swarmapi.TaskSpec_Container)
   640  	assert.Assert(t, ok)
   641  
   642  	assert.Check(t, is.Len(v.Container.Mounts, 1))
   643  	assert.Check(t, is.Equal(v.Container.Mounts[0].VolumeOptions.Subpath, "sub"))
   644  }