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 }