service_test.go 17 KB

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