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