github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/manager/controlapi/service_test.go (about) 1 package controlapi 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/docker/swarmkit/api" 12 "github.com/docker/swarmkit/identity" 13 "github.com/docker/swarmkit/manager/state/store" 14 "github.com/docker/swarmkit/testutils" 15 gogotypes "github.com/gogo/protobuf/types" 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 "google.golang.org/grpc/codes" 19 ) 20 21 func createGenericSpec(name, runtime string) *api.ServiceSpec { 22 spec := createSpec(name, runtime, 0) 23 spec.Task.Runtime = &api.TaskSpec_Generic{ 24 Generic: &api.GenericRuntimeSpec{ 25 Kind: runtime, 26 Payload: &gogotypes.Any{ 27 TypeUrl: "com.docker.custom.runtime", 28 Value: []byte{0}, 29 }, 30 }, 31 } 32 return spec 33 } 34 35 func createSpec(name, image string, instances uint64) *api.ServiceSpec { 36 return &api.ServiceSpec{ 37 Annotations: api.Annotations{ 38 Name: name, 39 Labels: map[string]string{ 40 "common": "yes", 41 "unique": name, 42 }, 43 }, 44 Task: api.TaskSpec{ 45 Runtime: &api.TaskSpec_Container{ 46 Container: &api.ContainerSpec{ 47 Image: image, 48 }, 49 }, 50 }, 51 Mode: &api.ServiceSpec_Replicated{ 52 Replicated: &api.ReplicatedService{ 53 Replicas: instances, 54 }, 55 }, 56 } 57 } 58 59 func createSpecWithDuplicateMounts(name string) *api.ServiceSpec { 60 service := createSpec("", "image", 1) 61 mounts := []api.Mount{ 62 { 63 Target: "/foo", 64 Source: "/mnt/mount1", 65 }, 66 { 67 Target: "/foo", 68 Source: "/mnt/mount2", 69 }, 70 } 71 72 service.Task.GetContainer().Mounts = mounts 73 74 return service 75 } 76 77 func createSpecWithHostnameTemplate(serviceName, hostnameTmpl string) *api.ServiceSpec { 78 service := createSpec(serviceName, "image", 1) 79 service.Task.GetContainer().Hostname = hostnameTmpl 80 return service 81 } 82 83 func createSecret(t *testing.T, ts *testServer, secretName, target string) *api.SecretReference { 84 secretSpec := createSecretSpec(secretName, []byte(secretName), nil) 85 secret := &api.Secret{ 86 ID: fmt.Sprintf("ID%v", secretName), 87 Spec: *secretSpec, 88 } 89 err := ts.Store.Update(func(tx store.Tx) error { 90 return store.CreateSecret(tx, secret) 91 }) 92 assert.NoError(t, err) 93 94 return &api.SecretReference{ 95 SecretName: secret.Spec.Annotations.Name, 96 SecretID: secret.ID, 97 Target: &api.SecretReference_File{ 98 File: &api.FileTarget{ 99 Name: target, 100 UID: "0", 101 GID: "0", 102 Mode: 0666, 103 }, 104 }, 105 } 106 } 107 108 func createServiceSpecWithSecrets(serviceName string, secretRefs ...*api.SecretReference) *api.ServiceSpec { 109 service := createSpec(serviceName, fmt.Sprintf("image%v", serviceName), 1) 110 service.Task.GetContainer().Secrets = secretRefs 111 112 return service 113 } 114 115 func createConfig(t *testing.T, ts *testServer, configName, target string) *api.ConfigReference { 116 configSpec := createConfigSpec(configName, []byte(configName), nil) 117 config := &api.Config{ 118 ID: fmt.Sprintf("ID%v", configName), 119 Spec: *configSpec, 120 } 121 err := ts.Store.Update(func(tx store.Tx) error { 122 return store.CreateConfig(tx, config) 123 }) 124 assert.NoError(t, err) 125 126 return &api.ConfigReference{ 127 ConfigName: config.Spec.Annotations.Name, 128 ConfigID: config.ID, 129 Target: &api.ConfigReference_File{ 130 File: &api.FileTarget{ 131 Name: target, 132 UID: "0", 133 GID: "0", 134 Mode: 0666, 135 }, 136 }, 137 } 138 } 139 140 func createServiceSpecWithConfigs(serviceName string, configRefs ...*api.ConfigReference) *api.ServiceSpec { 141 service := createSpec(serviceName, fmt.Sprintf("image%v", serviceName), 1) 142 service.Task.GetContainer().Configs = configRefs 143 144 return service 145 } 146 147 func createService(t *testing.T, ts *testServer, name, image string, instances uint64) *api.Service { 148 spec := createSpec(name, image, instances) 149 r, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 150 assert.NoError(t, err) 151 return r.Service 152 } 153 154 func createGenericService(t *testing.T, ts *testServer, name, runtime string) *api.Service { 155 spec := createGenericSpec(name, runtime) 156 r, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 157 assert.NoError(t, err) 158 return r.Service 159 } 160 161 func getIngressTargetID(t *testing.T, ts *testServer) string { 162 rsp, err := ts.Client.ListNetworks(context.Background(), &api.ListNetworksRequest{}) 163 assert.NoError(t, err) 164 for _, n := range rsp.Networks { 165 if n.Spec.Ingress { 166 return n.ID 167 } 168 } 169 t.Fatal("unable to find ingress") 170 return "" 171 } 172 173 func TestValidateResources(t *testing.T) { 174 bad := []*api.Resources{ 175 {MemoryBytes: 1}, 176 {NanoCPUs: 42}, 177 } 178 179 good := []*api.Resources{ 180 {MemoryBytes: 4096 * 1024 * 1024}, 181 {NanoCPUs: 1e9}, 182 } 183 184 for _, b := range bad { 185 err := validateResources(b) 186 assert.Error(t, err) 187 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 188 } 189 190 for _, g := range good { 191 assert.NoError(t, validateResources(g)) 192 } 193 } 194 195 func TestValidateResourceRequirements(t *testing.T) { 196 bad := []*api.ResourceRequirements{ 197 {Limits: &api.Resources{MemoryBytes: 1}}, 198 {Reservations: &api.Resources{MemoryBytes: 1}}, 199 } 200 good := []*api.ResourceRequirements{ 201 {Limits: &api.Resources{NanoCPUs: 1e9}}, 202 {Reservations: &api.Resources{NanoCPUs: 1e9}}, 203 } 204 for _, b := range bad { 205 err := validateResourceRequirements(b) 206 assert.Error(t, err) 207 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 208 } 209 210 for _, g := range good { 211 assert.NoError(t, validateResourceRequirements(g)) 212 } 213 } 214 215 func TestValidateMode(t *testing.T) { 216 negative := -4 217 bad := []*api.ServiceSpec{ 218 // -4 jammed into the replicas field, underflowing the uint64 219 {Mode: &api.ServiceSpec_Replicated{Replicated: &api.ReplicatedService{Replicas: uint64(negative)}}}, 220 {Mode: &api.ServiceSpec_ReplicatedJob{ReplicatedJob: &api.ReplicatedJob{MaxConcurrent: uint64(negative)}}}, 221 {Mode: &api.ServiceSpec_ReplicatedJob{ReplicatedJob: &api.ReplicatedJob{TotalCompletions: uint64(negative)}}}, 222 {}, 223 } 224 225 good := []*api.ServiceSpec{ 226 {Mode: &api.ServiceSpec_Replicated{Replicated: &api.ReplicatedService{Replicas: 2}}}, 227 {Mode: &api.ServiceSpec_Global{}}, 228 { 229 Mode: &api.ServiceSpec_ReplicatedJob{ 230 ReplicatedJob: &api.ReplicatedJob{ 231 MaxConcurrent: 3, TotalCompletions: 9, 232 }, 233 }, 234 }, 235 {Mode: &api.ServiceSpec_GlobalJob{}}, 236 } 237 238 for _, b := range bad { 239 err := validateMode(b) 240 assert.Error(t, err) 241 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 242 } 243 244 for _, g := range good { 245 err := validateMode(g) 246 assert.NoError(t, err) 247 } 248 } 249 250 func TestValidateTaskSpec(t *testing.T) { 251 type badSource struct { 252 s api.TaskSpec 253 c codes.Code 254 } 255 256 for _, bad := range []badSource{ 257 { 258 s: api.TaskSpec{ 259 Runtime: &api.TaskSpec_Container{ 260 Container: &api.ContainerSpec{}, 261 }, 262 }, 263 c: codes.InvalidArgument, 264 }, 265 { 266 s: api.TaskSpec{ 267 Runtime: &api.TaskSpec_Attachment{ 268 Attachment: &api.NetworkAttachmentSpec{}, 269 }, 270 }, 271 c: codes.Unimplemented, 272 }, 273 { 274 s: createSpec("", "", 0).Task, 275 c: codes.InvalidArgument, 276 }, 277 { 278 s: createSpec("", "busybox###", 0).Task, 279 c: codes.InvalidArgument, 280 }, 281 { 282 s: createGenericSpec("name", "").Task, 283 c: codes.InvalidArgument, 284 }, 285 { 286 s: createGenericSpec("name", "c").Task, 287 c: codes.InvalidArgument, 288 }, 289 { 290 s: createSpecWithDuplicateMounts("test").Task, 291 c: codes.InvalidArgument, 292 }, 293 { 294 s: createSpecWithHostnameTemplate("", "{{.Nothing.here}}").Task, 295 c: codes.InvalidArgument, 296 }, 297 } { 298 err := validateTaskSpec(bad.s) 299 assert.Error(t, err) 300 assert.Equal(t, bad.c, testutils.ErrorCode(err)) 301 } 302 303 for _, good := range []api.TaskSpec{ 304 createSpec("", "image", 0).Task, 305 createGenericSpec("", "custom").Task, 306 createSpecWithHostnameTemplate("service", "{{.Service.Name}}-{{.Task.Slot}}").Task, 307 } { 308 err := validateTaskSpec(good) 309 assert.NoError(t, err) 310 } 311 } 312 313 func TestValidateContainerSpec(t *testing.T) { 314 type BadSpec struct { 315 spec api.TaskSpec 316 c codes.Code 317 } 318 319 bad1 := api.TaskSpec{ 320 Runtime: &api.TaskSpec_Container{ 321 Container: &api.ContainerSpec{ 322 Image: "", // image name should not be empty 323 }, 324 }, 325 } 326 327 bad2 := api.TaskSpec{ 328 Runtime: &api.TaskSpec_Container{ 329 Container: &api.ContainerSpec{ 330 Image: "image", 331 Mounts: []api.Mount{ 332 { 333 Type: api.Mount_MountType(0), 334 Source: "/data", 335 Target: "/data", 336 }, 337 { 338 Type: api.Mount_MountType(0), 339 Source: "/data2", 340 Target: "/data", // duplicate mount point 341 }, 342 }, 343 }, 344 }, 345 } 346 347 bad3 := api.TaskSpec{ 348 Runtime: &api.TaskSpec_Container{ 349 Container: &api.ContainerSpec{ 350 Image: "image", 351 Healthcheck: &api.HealthConfig{ 352 Test: []string{"curl 127.0.0.1:3000"}, 353 Interval: gogotypes.DurationProto(time.Duration(-1 * time.Second)), // invalid negative duration 354 Timeout: gogotypes.DurationProto(time.Duration(-1 * time.Second)), // invalid negative duration 355 Retries: -1, // invalid negative integer 356 StartPeriod: gogotypes.DurationProto(time.Duration(-1 * time.Second)), // invalid negative duration 357 }, 358 }, 359 }, 360 } 361 362 for _, bad := range []BadSpec{ 363 { 364 spec: bad1, 365 c: codes.InvalidArgument, 366 }, 367 { 368 spec: bad2, 369 c: codes.InvalidArgument, 370 }, 371 { 372 spec: bad3, 373 c: codes.InvalidArgument, 374 }, 375 } { 376 err := validateContainerSpec(bad.spec) 377 assert.Error(t, err) 378 assert.Equal(t, bad.c, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 379 } 380 381 good1 := api.TaskSpec{ 382 Runtime: &api.TaskSpec_Container{ 383 Container: &api.ContainerSpec{ 384 Image: "image", 385 Mounts: []api.Mount{ 386 { 387 Type: api.Mount_MountType(0), 388 Source: "/data", 389 Target: "/data", 390 }, 391 { 392 Type: api.Mount_MountType(0), 393 Source: "/data2", 394 Target: "/data2", 395 }, 396 }, 397 Healthcheck: &api.HealthConfig{ 398 Test: []string{"curl 127.0.0.1:3000"}, 399 Interval: gogotypes.DurationProto(time.Duration(1 * time.Second)), 400 Timeout: gogotypes.DurationProto(time.Duration(3 * time.Second)), 401 Retries: 5, 402 StartPeriod: gogotypes.DurationProto(time.Duration(1 * time.Second)), 403 }, 404 }, 405 }, 406 } 407 408 for _, good := range []api.TaskSpec{good1} { 409 err := validateContainerSpec(good) 410 assert.NoError(t, err) 411 } 412 } 413 414 func TestValidateServiceSpec(t *testing.T) { 415 type BadServiceSpec struct { 416 spec *api.ServiceSpec 417 c codes.Code 418 } 419 420 for _, bad := range []BadServiceSpec{ 421 { 422 spec: nil, 423 c: codes.InvalidArgument, 424 }, 425 { 426 spec: &api.ServiceSpec{Annotations: api.Annotations{Name: "name"}}, 427 c: codes.InvalidArgument, 428 }, 429 { 430 spec: createSpec("", "", 1), 431 c: codes.InvalidArgument, 432 }, 433 { 434 spec: createSpec("name", "", 1), 435 c: codes.InvalidArgument, 436 }, 437 { 438 spec: createSpec("", "image", 1), 439 c: codes.InvalidArgument, 440 }, 441 { 442 spec: createSpec(strings.Repeat("longname", 8), "image", 1), 443 c: codes.InvalidArgument, 444 }, 445 } { 446 err := validateServiceSpec(bad.spec) 447 assert.Error(t, err) 448 assert.Equal(t, bad.c, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 449 } 450 451 for _, good := range []*api.ServiceSpec{ 452 createSpec("name", "image", 1), 453 } { 454 err := validateServiceSpec(good) 455 assert.NoError(t, err) 456 } 457 } 458 459 // TestValidateServiceSpecJobsDifference is different from 460 // TestValidateServiceSpec in that it checks that job-mode services are 461 // validated differently from regular services. 462 func TestValidateServiceSpecJobsDifference(t *testing.T) { 463 // correctly formed spec should be valid 464 cannedSpec := createSpec("name", "image", 1) 465 err := validateServiceSpec(cannedSpec) 466 assert.NoError(t, err) 467 468 // Replicated job should not be allowed to have update config 469 specReplicatedJobUpdate := cannedSpec.Copy() 470 specReplicatedJobUpdate.Mode = &api.ServiceSpec_ReplicatedJob{ 471 ReplicatedJob: &api.ReplicatedJob{}, 472 } 473 specReplicatedJobUpdate.Update = &api.UpdateConfig{} 474 err = validateServiceSpec(specReplicatedJobUpdate) 475 assert.Error(t, err) 476 477 specReplicatedJobNoUpdate := specReplicatedJobUpdate.Copy() 478 specReplicatedJobNoUpdate.Update = nil 479 err = validateServiceSpec(specReplicatedJobNoUpdate) 480 assert.NoError(t, err) 481 482 // Global job should not be allowed to have update config 483 specGlobalJobUpdate := cannedSpec.Copy() 484 specGlobalJobUpdate.Mode = &api.ServiceSpec_GlobalJob{ 485 GlobalJob: &api.GlobalJob{}, 486 } 487 specGlobalJobUpdate.Update = &api.UpdateConfig{} 488 err = validateServiceSpec(specGlobalJobUpdate) 489 assert.Error(t, err) 490 491 specGlobalJobNoUpdate := specGlobalJobUpdate.Copy() 492 specGlobalJobNoUpdate.Update = nil 493 err = validateServiceSpec(specReplicatedJobNoUpdate) 494 assert.NoError(t, err) 495 496 // Replicated service should be allowed to have update config, which should 497 // be verified for correctness 498 replicatedServiceBrokenUpdate := cannedSpec.Copy() 499 replicatedServiceBrokenUpdate.Update = &api.UpdateConfig{ 500 Delay: -1 * time.Second, 501 } 502 err = validateServiceSpec(replicatedServiceBrokenUpdate) 503 assert.Error(t, err) 504 505 replicatedServiceCorrectUpdate := replicatedServiceBrokenUpdate.Copy() 506 replicatedServiceCorrectUpdate.Update.Delay = time.Second 507 err = validateServiceSpec(replicatedServiceCorrectUpdate) 508 assert.NoError(t, err) 509 510 // Global service should be allowed to have update config, which should be 511 // verified for correctness 512 globalServiceBrokenUpdate := replicatedServiceBrokenUpdate.Copy() 513 globalServiceBrokenUpdate.Mode = &api.ServiceSpec_Global{ 514 Global: &api.GlobalService{}, 515 } 516 err = validateServiceSpec(globalServiceBrokenUpdate) 517 assert.Error(t, err) 518 519 globalServiceCorrectUpdate := globalServiceBrokenUpdate.Copy() 520 globalServiceCorrectUpdate.Update.Delay = time.Second 521 err = validateServiceSpec(globalServiceCorrectUpdate) 522 } 523 524 func TestValidateRestartPolicy(t *testing.T) { 525 bad := []*api.RestartPolicy{ 526 { 527 Delay: gogotypes.DurationProto(time.Duration(-1 * time.Second)), 528 Window: gogotypes.DurationProto(time.Duration(-1 * time.Second)), 529 }, 530 { 531 Delay: gogotypes.DurationProto(time.Duration(20 * time.Second)), 532 Window: gogotypes.DurationProto(time.Duration(-4 * time.Second)), 533 }, 534 } 535 536 good := []*api.RestartPolicy{ 537 { 538 Delay: gogotypes.DurationProto(time.Duration(10 * time.Second)), 539 Window: gogotypes.DurationProto(time.Duration(1 * time.Second)), 540 }, 541 } 542 543 for _, b := range bad { 544 err := validateRestartPolicy(b) 545 assert.Error(t, err) 546 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 547 } 548 549 for _, g := range good { 550 assert.NoError(t, validateRestartPolicy(g)) 551 } 552 } 553 554 func TestValidateUpdate(t *testing.T) { 555 bad := []*api.UpdateConfig{ 556 {Delay: -1 * time.Second}, 557 {Delay: -1000 * time.Second}, 558 {Monitor: gogotypes.DurationProto(time.Duration(-1 * time.Second))}, 559 {Monitor: gogotypes.DurationProto(time.Duration(-1000 * time.Second))}, 560 {MaxFailureRatio: -0.1}, 561 {MaxFailureRatio: 1.1}, 562 } 563 564 good := []*api.UpdateConfig{ 565 {Delay: time.Second}, 566 {Monitor: gogotypes.DurationProto(time.Duration(time.Second))}, 567 {MaxFailureRatio: 0.5}, 568 } 569 570 for _, b := range bad { 571 err := validateUpdate(b) 572 assert.Error(t, err) 573 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 574 } 575 576 for _, g := range good { 577 assert.NoError(t, validateUpdate(g)) 578 } 579 } 580 581 func TestCreateService(t *testing.T) { 582 ts := newTestServer(t) 583 defer ts.Stop() 584 _, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{}) 585 assert.Error(t, err) 586 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 587 588 spec := createSpec("name", "image", 1) 589 r, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 590 assert.NoError(t, err) 591 assert.NotEmpty(t, r.Service.ID) 592 593 // test port conflicts 594 spec = createSpec("name2", "image", 1) 595 spec.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 596 {PublishedPort: uint32(9000), TargetPort: uint32(9000), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 597 }} 598 r, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 599 assert.NoError(t, err) 600 assert.NotEmpty(t, r.Service.ID) 601 602 spec2 := createSpec("name3", "image", 1) 603 spec2.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 604 {PublishedPort: uint32(9000), TargetPort: uint32(9000), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 605 }} 606 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec2}) 607 assert.Error(t, err) 608 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 609 610 // test no port conflicts when no publish port is specified 611 spec3 := createSpec("name4", "image", 1) 612 spec3.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 613 {TargetPort: uint32(9000), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 614 }} 615 r, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec3}) 616 assert.NoError(t, err) 617 assert.NotEmpty(t, r.Service.ID) 618 spec4 := createSpec("name5", "image", 1) 619 spec4.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 620 {TargetPort: uint32(9001), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 621 }} 622 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec4}) 623 assert.NoError(t, err) 624 625 // ensure no port conflict when different protocols are used 626 spec = createSpec("name6", "image", 1) 627 spec.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 628 {PublishedPort: uint32(9100), TargetPort: uint32(9100), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 629 }} 630 r, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 631 assert.NoError(t, err) 632 assert.NotEmpty(t, r.Service.ID) 633 634 spec2 = createSpec("name7", "image", 1) 635 spec2.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 636 {PublishedPort: uint32(9100), TargetPort: uint32(9100), Protocol: api.PortConfig_Protocol(api.ProtocolUDP)}, 637 }} 638 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec2}) 639 assert.NoError(t, err) 640 641 // ensure no port conflict when host ports overlap 642 spec = createSpec("name8", "image", 1) 643 spec.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 644 {PublishMode: api.PublishModeHost, PublishedPort: uint32(9101), TargetPort: uint32(9101), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 645 }} 646 r, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 647 assert.NoError(t, err) 648 assert.NotEmpty(t, r.Service.ID) 649 650 spec2 = createSpec("name9", "image", 1) 651 spec2.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 652 {PublishMode: api.PublishModeHost, PublishedPort: uint32(9101), TargetPort: uint32(9101), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 653 }} 654 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec2}) 655 assert.NoError(t, err) 656 657 // ensure port conflict when host ports overlaps with ingress port (host port first) 658 spec = createSpec("name10", "image", 1) 659 spec.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 660 {PublishMode: api.PublishModeHost, PublishedPort: uint32(9102), TargetPort: uint32(9102), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 661 }} 662 r, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 663 assert.NoError(t, err) 664 assert.NotEmpty(t, r.Service.ID) 665 666 spec2 = createSpec("name11", "image", 1) 667 spec2.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 668 {PublishMode: api.PublishModeIngress, PublishedPort: uint32(9102), TargetPort: uint32(9102), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 669 }} 670 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec2}) 671 assert.Error(t, err) 672 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 673 674 // ensure port conflict when host ports overlaps with ingress port (ingress port first) 675 spec = createSpec("name12", "image", 1) 676 spec.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 677 {PublishMode: api.PublishModeIngress, PublishedPort: uint32(9103), TargetPort: uint32(9103), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 678 }} 679 r, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 680 assert.NoError(t, err) 681 assert.NotEmpty(t, r.Service.ID) 682 683 spec2 = createSpec("name13", "image", 1) 684 spec2.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 685 {PublishMode: api.PublishModeHost, PublishedPort: uint32(9103), TargetPort: uint32(9103), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 686 }} 687 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec2}) 688 assert.Error(t, err) 689 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 690 691 // ingress network cannot be attached explicitly 692 spec = createSpec("name14", "image", 1) 693 spec.Task.Networks = []*api.NetworkAttachmentConfig{{Target: getIngressTargetID(t, ts)}} 694 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 695 assert.Error(t, err) 696 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 697 698 spec = createSpec("notunique", "image", 1) 699 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 700 assert.NoError(t, err) 701 702 r, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 703 assert.Error(t, err) 704 assert.Equal(t, codes.AlreadyExists, testutils.ErrorCode(err)) 705 706 // Make sure the error contains "name conflicts with an existing object" for 707 // backward-compatibility with older clients doing string-matching... 708 assert.Contains(t, err.Error(), "name conflicts with an existing object") 709 } 710 711 func TestSecretValidation(t *testing.T) { 712 ts := newTestServer(t) 713 defer ts.Stop() 714 715 // test creating service with a secret that doesn't exist fails 716 secretRef := createSecret(t, ts, "secret", "secret.txt") 717 secretRef.SecretID = "404" 718 secretRef.SecretName = "404" 719 serviceSpec := createServiceSpecWithSecrets("service", secretRef) 720 _, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 721 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 722 723 // test creating service with a secretRef that has an existing secret 724 // but mismatched SecretName fails. 725 secretRef1 := createSecret(t, ts, "secret1", "secret1.txt") 726 secretRef1.SecretName = "secret2" 727 serviceSpec = createServiceSpecWithSecrets("service1", secretRef1) 728 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 729 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 730 731 // test secret target conflicts 732 secretRef2 := createSecret(t, ts, "secret2", "secret2.txt") 733 secretRef3 := createSecret(t, ts, "secret3", "secret2.txt") 734 serviceSpec = createServiceSpecWithSecrets("service2", secretRef2, secretRef3) 735 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 736 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 737 738 // test secret target conflicts with same secret and two references 739 secretRef3.SecretID = secretRef2.SecretID 740 secretRef3.SecretName = secretRef2.SecretName 741 serviceSpec = createServiceSpecWithSecrets("service3", secretRef2, secretRef3) 742 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 743 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 744 745 // test two different secretReferences with using the same secret 746 secretRef5 := secretRef2.Copy() 747 secretRef5.Target = &api.SecretReference_File{ 748 File: &api.FileTarget{ 749 Name: "different-target", 750 }, 751 } 752 753 serviceSpec = createServiceSpecWithSecrets("service4", secretRef2, secretRef5) 754 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 755 assert.NoError(t, err) 756 757 // test secret References with invalid filenames 758 secretRefBlank := createSecret(t, ts, "", "") 759 760 serviceSpec = createServiceSpecWithSecrets("invalid-blank", secretRefBlank) 761 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 762 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 763 764 // Test secret References with valid filenames 765 // Note: "../secretfile.txt", "../../secretfile.txt" will be rejected 766 // by the executor, but controlapi presently doesn't reject those names. 767 // Such validation would be platform-specific. 768 validFileNames := []string{"file.txt", ".file.txt", "_file-txt_.txt", "../secretfile.txt", "../../secretfile.txt", "file../.txt", "subdir/file.txt", "/file.txt"} 769 for i, validName := range validFileNames { 770 secretRef := createSecret(t, ts, validName, validName) 771 772 serviceSpec = createServiceSpecWithSecrets(fmt.Sprintf("valid%v", i), secretRef) 773 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 774 assert.NoError(t, err) 775 } 776 777 // test secret target conflicts on update 778 serviceSpec1 := createServiceSpecWithSecrets("service5", secretRef2, secretRef3) 779 // Copy this service, but delete the secrets for creation 780 serviceSpec2 := serviceSpec1.Copy() 781 serviceSpec2.Task.GetContainer().Secrets = nil 782 rs, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec2}) 783 assert.NoError(t, err) 784 785 // Attempt to update to the originally intended (conflicting) spec 786 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 787 ServiceID: rs.Service.ID, 788 Spec: serviceSpec1, 789 ServiceVersion: &rs.Service.Meta.Version, 790 }) 791 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 792 } 793 794 func TestConfigValidation(t *testing.T) { 795 ts := newTestServer(t) 796 defer ts.Stop() 797 798 // test creating service with a config that doesn't exist fails 799 configRef := createConfig(t, ts, "config", "config.txt") 800 configRef.ConfigID = "404" 801 configRef.ConfigName = "404" 802 serviceSpec := createServiceSpecWithConfigs("service", configRef) 803 _, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 804 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 805 806 // test creating service with a configRef that has an existing config 807 // but mismatched ConfigName fails. 808 configRef1 := createConfig(t, ts, "config1", "config1.txt") 809 configRef1.ConfigName = "config2" 810 serviceSpec = createServiceSpecWithConfigs("service1", configRef1) 811 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 812 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 813 814 // test config target conflicts 815 configRef2 := createConfig(t, ts, "config2", "config2.txt") 816 configRef3 := createConfig(t, ts, "config3", "config2.txt") 817 serviceSpec = createServiceSpecWithConfigs("service2", configRef2, configRef3) 818 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 819 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 820 821 // test config target conflicts with same config and two references 822 configRef3.ConfigID = configRef2.ConfigID 823 configRef3.ConfigName = configRef2.ConfigName 824 serviceSpec = createServiceSpecWithConfigs("service3", configRef2, configRef3) 825 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 826 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 827 828 // test two different configReferences with using the same config 829 configRef5 := configRef2.Copy() 830 configRef5.Target = &api.ConfigReference_File{ 831 File: &api.FileTarget{ 832 Name: "different-target", 833 }, 834 } 835 836 serviceSpec = createServiceSpecWithConfigs("service4", configRef2, configRef5) 837 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 838 assert.NoError(t, err) 839 840 // Test config References with valid filenames 841 // TODO(aaronl): Should some of these be disallowed? How can we deal 842 // with Windows-style paths on a Linux manager or vice versa? 843 validFileNames := []string{"../configfile.txt", "../../configfile.txt", "file../.txt", "subdir/file.txt", "file.txt", ".file.txt", "_file-txt_.txt"} 844 for i, validName := range validFileNames { 845 configRef := createConfig(t, ts, validName, validName) 846 847 serviceSpec = createServiceSpecWithConfigs(fmt.Sprintf("valid%v", i), configRef) 848 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}) 849 assert.NoError(t, err) 850 } 851 852 // test config references with RuntimeTarget 853 configRefCredSpec := createConfig(t, ts, "credentialspec", "credentialspec") 854 configRefCredSpec.Target = &api.ConfigReference_Runtime{ 855 Runtime: &api.RuntimeTarget{}, 856 } 857 serviceSpec = createServiceSpecWithConfigs("runtimetarget", configRefCredSpec) 858 serviceSpec.Task.GetContainer().Privileges = &api.Privileges{ 859 CredentialSpec: &api.Privileges_CredentialSpec{ 860 Source: &api.Privileges_CredentialSpec_Config{ 861 Config: configRefCredSpec.ConfigID, 862 }, 863 }, 864 } 865 _, err = ts.Client.CreateService( 866 context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}, 867 ) 868 assert.NoError(t, err) 869 870 // test CredentialSpec without ConfigReference 871 serviceSpec = createSpec("missingruntimetarget", "imagemissingruntimetarget", 1) 872 serviceSpec.Task.GetContainer().Privileges = &api.Privileges{ 873 CredentialSpec: &api.Privileges_CredentialSpec{ 874 Source: &api.Privileges_CredentialSpec_Config{ 875 Config: configRefCredSpec.ConfigID, 876 }, 877 }, 878 } 879 _, err = ts.Client.CreateService( 880 context.Background(), &api.CreateServiceRequest{Spec: serviceSpec}, 881 ) 882 t.Logf("error when missing configreference: %v", err) 883 assert.Error(t, err) 884 885 // test config target conflicts on update 886 serviceSpec1 := createServiceSpecWithConfigs("service5", configRef2, configRef3) 887 // Copy this service, but delete the configs for creation 888 serviceSpec2 := serviceSpec1.Copy() 889 serviceSpec2.Task.GetContainer().Configs = nil 890 rs, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: serviceSpec2}) 891 assert.NoError(t, err) 892 893 // Attempt to update to the originally intended (conflicting) spec 894 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 895 ServiceID: rs.Service.ID, 896 Spec: serviceSpec1, 897 ServiceVersion: &rs.Service.Meta.Version, 898 }) 899 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 900 } 901 902 func TestGetService(t *testing.T) { 903 ts := newTestServer(t) 904 defer ts.Stop() 905 _, err := ts.Client.GetService(context.Background(), &api.GetServiceRequest{}) 906 assert.Error(t, err) 907 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 908 909 _, err = ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: "invalid"}) 910 assert.Error(t, err) 911 assert.Equal(t, codes.NotFound, testutils.ErrorCode(err)) 912 913 service := createService(t, ts, "name", "image", 1) 914 r, err := ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: service.ID}) 915 assert.NoError(t, err) 916 service.Meta.Version = r.Service.Meta.Version 917 assert.Equal(t, service, r.Service) 918 } 919 920 func TestUpdateService(t *testing.T) { 921 ts := newTestServer(t) 922 defer ts.Stop() 923 service := createService(t, ts, "name", "image", 1) 924 925 _, err := ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{}) 926 assert.Error(t, err) 927 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 928 929 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ServiceID: "invalid", Spec: &service.Spec, ServiceVersion: &api.Version{}}) 930 assert.Error(t, err) 931 assert.Equal(t, codes.NotFound, testutils.ErrorCode(err)) 932 933 // No update options. 934 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ServiceID: service.ID, Spec: &service.Spec}) 935 assert.Error(t, err) 936 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 937 938 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ServiceID: service.ID, Spec: &service.Spec, ServiceVersion: &service.Meta.Version}) 939 assert.NoError(t, err) 940 941 r, err := ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: service.ID}) 942 assert.NoError(t, err) 943 assert.Equal(t, service.Spec.Annotations.Name, r.Service.Spec.Annotations.Name) 944 mode, ok := r.Service.Spec.GetMode().(*api.ServiceSpec_Replicated) 945 assert.Equal(t, ok, true) 946 assert.True(t, mode.Replicated.Replicas == 1) 947 948 mode.Replicated.Replicas = 42 949 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 950 ServiceID: service.ID, 951 Spec: &r.Service.Spec, 952 ServiceVersion: &r.Service.Meta.Version, 953 }) 954 assert.NoError(t, err) 955 956 r, err = ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: service.ID}) 957 assert.NoError(t, err) 958 assert.Equal(t, service.Spec.Annotations.Name, r.Service.Spec.Annotations.Name) 959 mode, ok = r.Service.Spec.GetMode().(*api.ServiceSpec_Replicated) 960 assert.Equal(t, ok, true) 961 assert.True(t, mode.Replicated.Replicas == 42) 962 963 // mode change not allowed 964 r, err = ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: service.ID}) 965 assert.NoError(t, err) 966 r.Service.Spec.Mode = &api.ServiceSpec_Global{ 967 Global: &api.GlobalService{}, 968 } 969 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 970 ServiceID: service.ID, 971 Spec: &r.Service.Spec, 972 ServiceVersion: &r.Service.Meta.Version, 973 }) 974 assert.Error(t, err) 975 assert.True(t, strings.Contains(err.Error(), errModeChangeNotAllowed.Error())) 976 977 // Versioning. 978 r, err = ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: service.ID}) 979 assert.NoError(t, err) 980 version := &r.Service.Meta.Version 981 982 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 983 ServiceID: service.ID, 984 Spec: &r.Service.Spec, 985 ServiceVersion: version, 986 }) 987 assert.NoError(t, err) 988 989 // Perform an update with the "old" version. 990 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 991 ServiceID: service.ID, 992 Spec: &r.Service.Spec, 993 ServiceVersion: version, 994 }) 995 assert.Error(t, err) 996 997 // Attempt to update service name; renaming is not implemented 998 r.Service.Spec.Annotations.Name = "newname" 999 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 1000 ServiceID: service.ID, 1001 Spec: &r.Service.Spec, 1002 ServiceVersion: version, 1003 }) 1004 assert.Error(t, err) 1005 assert.Equal(t, codes.Unimplemented, testutils.ErrorCode(err)) 1006 1007 // test port conflicts 1008 spec2 := createSpec("name2", "image", 1) 1009 spec2.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 1010 {PublishedPort: uint32(9000), TargetPort: uint32(9000), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 1011 }} 1012 _, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec2}) 1013 assert.NoError(t, err) 1014 1015 spec3 := createSpec("name3", "image", 1) 1016 rs, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec3}) 1017 assert.NoError(t, err) 1018 1019 spec3.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 1020 {PublishedPort: uint32(9000), TargetPort: uint32(9000), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 1021 }} 1022 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 1023 ServiceID: rs.Service.ID, 1024 Spec: spec3, 1025 ServiceVersion: &rs.Service.Meta.Version, 1026 }) 1027 assert.Error(t, err) 1028 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 1029 spec3.Endpoint = &api.EndpointSpec{Ports: []*api.PortConfig{ 1030 {PublishedPort: uint32(9001), TargetPort: uint32(9000), Protocol: api.PortConfig_Protocol(api.ProtocolTCP)}, 1031 }} 1032 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 1033 ServiceID: rs.Service.ID, 1034 Spec: spec3, 1035 ServiceVersion: &rs.Service.Meta.Version, 1036 }) 1037 assert.NoError(t, err) 1038 1039 // ingress network cannot be attached explicitly 1040 spec4 := createSpec("name4", "image", 1) 1041 rs, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec4}) 1042 assert.NoError(t, err) 1043 spec4.Task.Networks = []*api.NetworkAttachmentConfig{{Target: getIngressTargetID(t, ts)}} 1044 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 1045 ServiceID: rs.Service.ID, 1046 Spec: spec4, 1047 ServiceVersion: &rs.Service.Meta.Version, 1048 }) 1049 assert.Error(t, err) 1050 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 1051 } 1052 1053 func TestServiceUpdateRejectNetworkChange(t *testing.T) { 1054 ts := newTestServer(t) 1055 defer ts.Stop() 1056 spec := createSpec("name1", "image", 1) 1057 spec.Networks = []*api.NetworkAttachmentConfig{ 1058 { 1059 Target: "net20", 1060 }, 1061 } 1062 cr, err := ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 1063 assert.NoError(t, err) 1064 1065 ur, err := ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: cr.Service.ID}) 1066 assert.NoError(t, err) 1067 service := ur.Service 1068 1069 service.Spec.Networks[0].Target = "net30" 1070 1071 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 1072 ServiceID: service.ID, 1073 Spec: &service.Spec, 1074 ServiceVersion: &service.Meta.Version, 1075 }) 1076 assert.Error(t, err) 1077 assert.True(t, strings.Contains(err.Error(), errNetworkUpdateNotSupported.Error())) 1078 1079 // Changes to TaskSpec.Networks are allowed 1080 spec = createSpec("name2", "image", 1) 1081 spec.Task.Networks = []*api.NetworkAttachmentConfig{ 1082 { 1083 Target: "net20", 1084 }, 1085 } 1086 cr, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 1087 assert.NoError(t, err) 1088 1089 ur, err = ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: cr.Service.ID}) 1090 assert.NoError(t, err) 1091 service = ur.Service 1092 1093 service.Spec.Task.Networks[0].Target = "net30" 1094 1095 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 1096 ServiceID: service.ID, 1097 Spec: &service.Spec, 1098 ServiceVersion: &service.Meta.Version, 1099 }) 1100 assert.NoError(t, err) 1101 1102 // Migrate networks from ServiceSpec.Networks to TaskSpec.Networks 1103 spec = createSpec("name3", "image", 1) 1104 spec.Networks = []*api.NetworkAttachmentConfig{ 1105 { 1106 Target: "net20", 1107 }, 1108 } 1109 cr, err = ts.Client.CreateService(context.Background(), &api.CreateServiceRequest{Spec: spec}) 1110 assert.NoError(t, err) 1111 1112 ur, err = ts.Client.GetService(context.Background(), &api.GetServiceRequest{ServiceID: cr.Service.ID}) 1113 assert.NoError(t, err) 1114 service = ur.Service 1115 1116 service.Spec.Task.Networks = spec.Networks 1117 service.Spec.Networks = nil 1118 1119 _, err = ts.Client.UpdateService(context.Background(), &api.UpdateServiceRequest{ 1120 ServiceID: service.ID, 1121 Spec: &service.Spec, 1122 ServiceVersion: &service.Meta.Version, 1123 }) 1124 assert.NoError(t, err) 1125 } 1126 1127 func TestRemoveService(t *testing.T) { 1128 ts := newTestServer(t) 1129 defer ts.Stop() 1130 _, err := ts.Client.RemoveService(context.Background(), &api.RemoveServiceRequest{}) 1131 assert.Error(t, err) 1132 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 1133 1134 service := createService(t, ts, "name", "image", 1) 1135 r, err := ts.Client.RemoveService(context.Background(), &api.RemoveServiceRequest{ServiceID: service.ID}) 1136 assert.NoError(t, err) 1137 assert.NotNil(t, r) 1138 } 1139 1140 func TestValidateEndpointSpec(t *testing.T) { 1141 endPointSpec1 := &api.EndpointSpec{ 1142 Mode: api.ResolutionModeDNSRoundRobin, 1143 Ports: []*api.PortConfig{ 1144 { 1145 Name: "http", 1146 TargetPort: 80, 1147 }, 1148 }, 1149 } 1150 1151 endPointSpec2 := &api.EndpointSpec{ 1152 Mode: api.ResolutionModeVirtualIP, 1153 Ports: []*api.PortConfig{ 1154 { 1155 Name: "http", 1156 TargetPort: 81, 1157 PublishedPort: 8001, 1158 }, 1159 { 1160 Name: "http", 1161 TargetPort: 80, 1162 PublishedPort: 8000, 1163 }, 1164 }, 1165 } 1166 1167 // has duplicated published port, invalid 1168 endPointSpec3 := &api.EndpointSpec{ 1169 Mode: api.ResolutionModeVirtualIP, 1170 Ports: []*api.PortConfig{ 1171 { 1172 Name: "http", 1173 TargetPort: 81, 1174 PublishedPort: 8001, 1175 }, 1176 { 1177 Name: "http", 1178 TargetPort: 80, 1179 PublishedPort: 8001, 1180 }, 1181 }, 1182 } 1183 1184 // duplicated published port but different protocols, valid 1185 endPointSpec4 := &api.EndpointSpec{ 1186 Mode: api.ResolutionModeVirtualIP, 1187 Ports: []*api.PortConfig{ 1188 { 1189 Name: "dns", 1190 TargetPort: 53, 1191 PublishedPort: 8002, 1192 Protocol: api.ProtocolTCP, 1193 }, 1194 { 1195 Name: "dns", 1196 TargetPort: 53, 1197 PublishedPort: 8002, 1198 Protocol: api.ProtocolUDP, 1199 }, 1200 }, 1201 } 1202 1203 // multiple randomly assigned published ports 1204 endPointSpec5 := &api.EndpointSpec{ 1205 Mode: api.ResolutionModeVirtualIP, 1206 Ports: []*api.PortConfig{ 1207 { 1208 Name: "http", 1209 TargetPort: 80, 1210 Protocol: api.ProtocolTCP, 1211 }, 1212 { 1213 Name: "dns", 1214 TargetPort: 53, 1215 Protocol: api.ProtocolUDP, 1216 }, 1217 { 1218 Name: "dns", 1219 TargetPort: 53, 1220 Protocol: api.ProtocolTCP, 1221 }, 1222 }, 1223 } 1224 1225 err := validateEndpointSpec(endPointSpec1) 1226 assert.Error(t, err) 1227 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 1228 1229 err = validateEndpointSpec(endPointSpec2) 1230 assert.NoError(t, err) 1231 1232 err = validateEndpointSpec(endPointSpec3) 1233 assert.Error(t, err) 1234 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 1235 1236 err = validateEndpointSpec(endPointSpec4) 1237 assert.NoError(t, err) 1238 1239 err = validateEndpointSpec(endPointSpec5) 1240 assert.NoError(t, err) 1241 } 1242 1243 func TestServiceEndpointSpecUpdate(t *testing.T) { 1244 ts := newTestServer(t) 1245 defer ts.Stop() 1246 spec := &api.ServiceSpec{ 1247 Annotations: api.Annotations{ 1248 Name: "name", 1249 }, 1250 Task: api.TaskSpec{ 1251 Runtime: &api.TaskSpec_Container{ 1252 Container: &api.ContainerSpec{ 1253 Image: "image", 1254 }, 1255 }, 1256 }, 1257 Mode: &api.ServiceSpec_Replicated{ 1258 Replicated: &api.ReplicatedService{ 1259 Replicas: 1, 1260 }, 1261 }, 1262 Endpoint: &api.EndpointSpec{ 1263 Ports: []*api.PortConfig{ 1264 { 1265 Name: "http", 1266 TargetPort: 80, 1267 }, 1268 }, 1269 }, 1270 } 1271 1272 r, err := ts.Client.CreateService(context.Background(), 1273 &api.CreateServiceRequest{Spec: spec}) 1274 assert.NoError(t, err) 1275 assert.NotNil(t, r) 1276 1277 // Update the service with duplicate ports 1278 spec.Endpoint.Ports = append(spec.Endpoint.Ports, &api.PortConfig{ 1279 Name: "fakehttp", 1280 TargetPort: 80, 1281 }) 1282 _, err = ts.Client.UpdateService(context.Background(), 1283 &api.UpdateServiceRequest{Spec: spec}) 1284 assert.Error(t, err) 1285 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 1286 } 1287 1288 func TestListServices(t *testing.T) { 1289 ts := newTestServer(t) 1290 defer ts.Stop() 1291 r, err := ts.Client.ListServices(context.Background(), &api.ListServicesRequest{}) 1292 assert.NoError(t, err) 1293 assert.Empty(t, r.Services) 1294 1295 s1 := createService(t, ts, "name1", "image", 1) 1296 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{}) 1297 assert.NoError(t, err) 1298 assert.Equal(t, 1, len(r.Services)) 1299 1300 createService(t, ts, "name2", "image", 1) 1301 s3 := createGenericService(t, ts, "name3", "my-runtime") 1302 1303 // List all. 1304 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{}) 1305 assert.NoError(t, err) 1306 assert.Equal(t, 3, len(r.Services)) 1307 1308 // List by runtime. 1309 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{ 1310 Filters: &api.ListServicesRequest_Filters{ 1311 Runtimes: []string{"container"}, 1312 }, 1313 }) 1314 assert.NoError(t, err) 1315 assert.Equal(t, 2, len(r.Services)) 1316 1317 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{ 1318 Filters: &api.ListServicesRequest_Filters{ 1319 Runtimes: []string{"my-runtime"}, 1320 }, 1321 }) 1322 assert.NoError(t, err) 1323 assert.Equal(t, 1, len(r.Services)) 1324 assert.Equal(t, s3.ID, r.Services[0].ID) 1325 1326 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{ 1327 Filters: &api.ListServicesRequest_Filters{ 1328 Runtimes: []string{"invalid"}, 1329 }, 1330 }) 1331 assert.NoError(t, err) 1332 assert.Empty(t, r.Services) 1333 1334 // List with an ID prefix. 1335 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{ 1336 Filters: &api.ListServicesRequest_Filters{ 1337 IDPrefixes: []string{s1.ID[0:4]}, 1338 }, 1339 }) 1340 assert.NoError(t, err) 1341 assert.Equal(t, 1, len(r.Services)) 1342 assert.Equal(t, s1.ID, r.Services[0].ID) 1343 1344 // List with simple filter. 1345 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{ 1346 Filters: &api.ListServicesRequest_Filters{ 1347 NamePrefixes: []string{"name1"}, 1348 }, 1349 }) 1350 assert.NoError(t, err) 1351 assert.Equal(t, 1, len(r.Services)) 1352 1353 // List with union filter. 1354 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{ 1355 Filters: &api.ListServicesRequest_Filters{ 1356 NamePrefixes: []string{"name1", "name2"}, 1357 }, 1358 }) 1359 assert.NoError(t, err) 1360 assert.Equal(t, 2, len(r.Services)) 1361 1362 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{ 1363 Filters: &api.ListServicesRequest_Filters{ 1364 NamePrefixes: []string{"name1", "name2", "name4"}, 1365 }, 1366 }) 1367 assert.NoError(t, err) 1368 assert.Equal(t, 2, len(r.Services)) 1369 1370 r, err = ts.Client.ListServices(context.Background(), &api.ListServicesRequest{ 1371 Filters: &api.ListServicesRequest_Filters{ 1372 NamePrefixes: []string{"name4"}, 1373 }, 1374 }) 1375 assert.NoError(t, err) 1376 assert.Equal(t, 0, len(r.Services)) 1377 1378 // List with filter intersection. 1379 r, err = ts.Client.ListServices(context.Background(), 1380 &api.ListServicesRequest{ 1381 Filters: &api.ListServicesRequest_Filters{ 1382 NamePrefixes: []string{"name1"}, 1383 IDPrefixes: []string{s1.ID}, 1384 }, 1385 }, 1386 ) 1387 assert.NoError(t, err) 1388 assert.Equal(t, 1, len(r.Services)) 1389 1390 r, err = ts.Client.ListServices(context.Background(), 1391 &api.ListServicesRequest{ 1392 Filters: &api.ListServicesRequest_Filters{ 1393 NamePrefixes: []string{"name2"}, 1394 IDPrefixes: []string{s1.ID}, 1395 }, 1396 }, 1397 ) 1398 assert.NoError(t, err) 1399 assert.Equal(t, 0, len(r.Services)) 1400 1401 r, err = ts.Client.ListServices(context.Background(), 1402 &api.ListServicesRequest{ 1403 Filters: &api.ListServicesRequest_Filters{ 1404 NamePrefixes: []string{"name3"}, 1405 Runtimes: []string{"my-runtime"}, 1406 }, 1407 }, 1408 ) 1409 assert.NoError(t, err) 1410 assert.Equal(t, 1, len(r.Services)) 1411 1412 // List filter by label. 1413 r, err = ts.Client.ListServices(context.Background(), 1414 &api.ListServicesRequest{ 1415 Filters: &api.ListServicesRequest_Filters{ 1416 Labels: map[string]string{ 1417 "common": "yes", 1418 }, 1419 }, 1420 }, 1421 ) 1422 assert.NoError(t, err) 1423 assert.Equal(t, 3, len(r.Services)) 1424 1425 // Value-less label. 1426 r, err = ts.Client.ListServices(context.Background(), 1427 &api.ListServicesRequest{ 1428 Filters: &api.ListServicesRequest_Filters{ 1429 Labels: map[string]string{ 1430 "common": "", 1431 }, 1432 }, 1433 }, 1434 ) 1435 assert.NoError(t, err) 1436 assert.Equal(t, 3, len(r.Services)) 1437 1438 // Label intersection. 1439 r, err = ts.Client.ListServices(context.Background(), 1440 &api.ListServicesRequest{ 1441 Filters: &api.ListServicesRequest_Filters{ 1442 Labels: map[string]string{ 1443 "common": "", 1444 "unique": "name1", 1445 }, 1446 }, 1447 }, 1448 ) 1449 assert.NoError(t, err) 1450 assert.Equal(t, 1, len(r.Services)) 1451 1452 r, err = ts.Client.ListServices(context.Background(), 1453 &api.ListServicesRequest{ 1454 Filters: &api.ListServicesRequest_Filters{ 1455 Labels: map[string]string{ 1456 "common": "", 1457 "unique": "error", 1458 }, 1459 }, 1460 }, 1461 ) 1462 assert.NoError(t, err) 1463 assert.Equal(t, 0, len(r.Services)) 1464 } 1465 1466 func TestListServiceStatuses(t *testing.T) { 1467 ts := newTestServer(t) 1468 defer ts.Stop() 1469 1470 // Test listing no services is empty and has no error 1471 r, err := ts.Client.ListServiceStatuses( 1472 context.Background(), 1473 &api.ListServiceStatusesRequest{}, 1474 ) 1475 assert.NoError(t, err, "error when listing no services against an empty store") 1476 assert.NotNil(t, r, "response against an empty store was nil") 1477 assert.Empty(t, r.Statuses, "response statuses was not empty") 1478 1479 // Test listing services that do not exist. We should still get a response, 1480 // but both desired and actual should be 0 1481 r, err = ts.Client.ListServiceStatuses( 1482 context.Background(), 1483 &api.ListServiceStatusesRequest{Services: []string{"foo"}}, 1484 ) 1485 assert.NoError(t, err, "error listing services that do not exist") 1486 assert.NotNil(t, r, "response for nonexistant services was nil") 1487 assert.Len(t, r.Statuses, 1, "expected 1 status") 1488 assert.Equal( 1489 t, r.Statuses[0], 1490 &api.ListServiceStatusesResponse_ServiceStatus{ServiceID: "foo"}, 1491 ) 1492 1493 // now test that listing service statuses actually works. 1494 1495 // justRight will be converged 1496 justRight := createService(t, ts, "justRight", "image", 3) 1497 // notEnough will not have enough tasks in running 1498 notEnough := createService(t, ts, "notEnough", "image", 7) 1499 1500 // no shortcut for creating a global service 1501 globalSpec := createSpec("global", "image", 0) 1502 globalSpec.Mode = &api.ServiceSpec_Global{Global: &api.GlobalService{}} 1503 1504 svcResp, svcErr := ts.Client.CreateService( 1505 context.Background(), &api.CreateServiceRequest{Spec: globalSpec}, 1506 ) 1507 assert.NoError(t, svcErr) 1508 1509 // global will have the right number of tasks 1510 global := svcResp.Service 1511 1512 global2Spec := globalSpec.Copy() 1513 global2Spec.Annotations.Name = "global2" 1514 1515 svcResp, svcErr = ts.Client.CreateService( 1516 context.Background(), &api.CreateServiceRequest{Spec: global2Spec}, 1517 ) 1518 assert.NoError(t, svcErr) 1519 1520 // global2 will not have enough tasks 1521 global2 := svcResp.Service 1522 1523 // over will have too many tasks running (as would be seen in a scale-down 1524 over := createService(t, ts, "over", "image", 2) 1525 1526 // replicatedJob1 will be partly completed 1527 replicatedJob1Spec := createSpec("replicatedJob1", "image", 0) 1528 replicatedJob1Spec.Mode = &api.ServiceSpec_ReplicatedJob{ 1529 ReplicatedJob: &api.ReplicatedJob{ 1530 MaxConcurrent: 2, 1531 TotalCompletions: 10, 1532 }, 1533 } 1534 1535 svcResp, svcErr = ts.Client.CreateService( 1536 context.Background(), &api.CreateServiceRequest{Spec: replicatedJob1Spec}, 1537 ) 1538 assert.NoError(t, svcErr) 1539 assert.NotNil(t, svcResp) 1540 replicatedJob1 := svcResp.Service 1541 1542 // replicatedJob2 has been executed before, and will have tasks from a 1543 // previous JobIteration 1544 replicatedJob2Spec := replicatedJob1Spec.Copy() 1545 replicatedJob2Spec.Annotations.Name = "replicatedJob2" 1546 svcResp, svcErr = ts.Client.CreateService( 1547 context.Background(), &api.CreateServiceRequest{Spec: replicatedJob2Spec}, 1548 ) 1549 assert.NoError(t, svcErr) 1550 assert.NotNil(t, svcResp) 1551 replicatedJob2 := svcResp.Service 1552 1553 // globalJob is a partly complete global job 1554 globalJobSpec := createSpec("globalJob", "image", 0) 1555 globalJobSpec.Mode = &api.ServiceSpec_GlobalJob{ 1556 GlobalJob: &api.GlobalJob{}, 1557 } 1558 svcResp, svcErr = ts.Client.CreateService( 1559 context.Background(), &api.CreateServiceRequest{Spec: globalJobSpec}, 1560 ) 1561 assert.NoError(t, svcErr) 1562 assert.NotNil(t, svcResp) 1563 globalJob := svcResp.Service 1564 1565 // now create some tasks. use a quick helper function for this 1566 createTask := func(s *api.Service, actual api.TaskState, desired api.TaskState, opts ...func(*api.Service, *api.Task)) *api.Task { 1567 task := &api.Task{ 1568 ID: identity.NewID(), 1569 DesiredState: desired, 1570 Spec: s.Spec.Task, 1571 Status: api.TaskStatus{ 1572 State: actual, 1573 }, 1574 ServiceID: s.ID, 1575 } 1576 1577 for _, opt := range opts { 1578 opt(s, task) 1579 } 1580 1581 err := ts.Store.Update(func(tx store.Tx) error { 1582 return store.CreateTask(tx, task) 1583 }) 1584 assert.NoError(t, err) 1585 return task 1586 } 1587 1588 withJobIteration := func(s *api.Service, task *api.Task) { 1589 assert.NotNil(t, s.JobStatus) 1590 task.JobIteration = &(s.JobStatus.JobIteration) 1591 } 1592 1593 // alias task states for brevity 1594 running := api.TaskStateRunning 1595 shutdown := api.TaskStateShutdown 1596 completed := api.TaskStateCompleted 1597 newt := api.TaskStateNew 1598 failed := api.TaskStateFailed 1599 1600 // create 3 running tasks for justRight 1601 for i := 0; i < 3; i++ { 1602 createTask(justRight, running, running) 1603 } 1604 // create 2 failed and 2 shutdown tasks 1605 for i := 0; i < 2; i++ { 1606 createTask(justRight, failed, shutdown) 1607 createTask(justRight, shutdown, shutdown) 1608 } 1609 1610 // create 4 tasks for notEnough 1611 for i := 0; i < 4; i++ { 1612 createTask(notEnough, running, running) 1613 } 1614 // create 3 tasks in new state 1615 for i := 0; i < 3; i++ { 1616 createTask(notEnough, newt, running) 1617 } 1618 // create 1 failed and 1 shutdown task 1619 createTask(notEnough, failed, shutdown) 1620 createTask(notEnough, shutdown, shutdown) 1621 1622 // create 2 tasks out of 2 desired for global 1623 for i := 0; i < 2; i++ { 1624 createTask(global, running, running) 1625 } 1626 // create 3 shutdown tasks for global 1627 for i := 0; i < 3; i++ { 1628 createTask(global, shutdown, shutdown) 1629 } 1630 1631 // create 4 out of 5 tasks for global2 1632 for i := 0; i < 4; i++ { 1633 createTask(global2, running, running) 1634 } 1635 createTask(global2, newt, running) 1636 1637 // create 6 failed tasks 1638 for i := 0; i < 6; i++ { 1639 createTask(global2, failed, shutdown) 1640 } 1641 1642 // create 4 out of 2 tasks. no shutdown or failed tasks. this would be the 1643 // case if you did a call immediately after updating the service, before 1644 // the orchestrator had updated the task desired states 1645 for i := 0; i < 4; i++ { 1646 createTask(over, running, running) 1647 } 1648 1649 // create 2 running tasks for replicatedJob1 1650 for i := 0; i < 2; i++ { 1651 createTask(replicatedJob1, running, completed, withJobIteration) 1652 } 1653 1654 // create 4 completed tasks for replicatedJob1 1655 for i := 0; i < 4; i++ { 1656 createTask(replicatedJob1, completed, completed, withJobIteration) 1657 } 1658 1659 // create 10 completed tasks for replicatedJob2 1660 for i := 0; i < 10; i++ { 1661 createTask(replicatedJob2, completed, completed, withJobIteration) 1662 } 1663 1664 replicatedJob2Spec.Task.ForceUpdate++ 1665 1666 // now update replicatedJob2, so JobIteration gets incremented 1667 updateResp, updateErr := ts.Client.UpdateService( 1668 context.Background(), 1669 &api.UpdateServiceRequest{ 1670 ServiceID: replicatedJob2.ID, 1671 ServiceVersion: &replicatedJob2.Meta.Version, 1672 Spec: replicatedJob2Spec, 1673 }, 1674 ) 1675 assert.NoError(t, updateErr) 1676 assert.NotNil(t, updateResp) 1677 replicatedJob2 = updateResp.Service 1678 1679 // and create 1 tasks out of 2 1680 createTask(replicatedJob2, running, completed, withJobIteration) 1681 // and 3 completed already 1682 for i := 0; i < 3; i++ { 1683 createTask(replicatedJob2, completed, completed, withJobIteration) 1684 } 1685 1686 // create 5 running tasks for globalJob 1687 for i := 0; i < 5; i++ { 1688 createTask(globalJob, running, completed, withJobIteration) 1689 } 1690 // create 3 completed tasks 1691 for i := 0; i < 3; i++ { 1692 createTask(globalJob, completed, completed, withJobIteration) 1693 } 1694 1695 // now, create a service that has already been deleted, but has dangling 1696 // tasks 1697 goneSpec := createSpec("gone", "image", 3) 1698 gone := &api.Service{ 1699 ID: identity.NewID(), 1700 Spec: *goneSpec, 1701 } 1702 1703 for i := 0; i < 3; i++ { 1704 createTask(gone, running, shutdown) 1705 createTask(gone, shutdown, shutdown) 1706 } 1707 1708 // now list service statuses 1709 r, err = ts.Client.ListServiceStatuses( 1710 context.Background(), 1711 &api.ListServiceStatusesRequest{Services: []string{ 1712 justRight.ID, notEnough.ID, global.ID, global2.ID, 1713 replicatedJob1.ID, replicatedJob2.ID, globalJob.ID, over.ID, gone.ID, 1714 }}, 1715 ) 1716 assert.NoError(t, err, "error getting service statuses") 1717 assert.NotNil(t, r, "service status response is nil") 1718 assert.Len(t, r.Statuses, 9) 1719 1720 expected := map[string]*api.ListServiceStatusesResponse_ServiceStatus{ 1721 "justRight": { 1722 ServiceID: justRight.ID, 1723 DesiredTasks: 3, 1724 RunningTasks: 3, 1725 }, 1726 "notEnough": { 1727 ServiceID: notEnough.ID, 1728 DesiredTasks: 7, 1729 RunningTasks: 4, 1730 }, 1731 "global": { 1732 ServiceID: global.ID, 1733 DesiredTasks: 2, 1734 RunningTasks: 2, 1735 }, 1736 "global2": { 1737 ServiceID: global2.ID, 1738 DesiredTasks: 5, 1739 RunningTasks: 4, 1740 }, 1741 "over": { 1742 ServiceID: over.ID, 1743 DesiredTasks: 2, 1744 RunningTasks: 4, 1745 }, 1746 "replicatedJob1": { 1747 ServiceID: replicatedJob1.ID, 1748 DesiredTasks: 2, 1749 RunningTasks: 2, 1750 CompletedTasks: 4, 1751 }, 1752 "replicatedJob2": { 1753 ServiceID: replicatedJob2.ID, 1754 DesiredTasks: 2, 1755 RunningTasks: 1, 1756 CompletedTasks: 3, 1757 }, 1758 "globalJob": { 1759 ServiceID: globalJob.ID, 1760 DesiredTasks: 5, 1761 RunningTasks: 5, 1762 CompletedTasks: 3, 1763 }, 1764 "gone": { 1765 ServiceID: gone.ID, 1766 DesiredTasks: 0, 1767 RunningTasks: 3, 1768 }, 1769 } 1770 1771 // compare expected and actual values. make sure all are used by keeping 1772 // track of which we visited. i borrowed this pattern from 1773 // assert.ElementsMatch, which is in a newer version of that library 1774 visited := make([]bool, len(expected)) 1775 for name, expect := range expected { 1776 found := false 1777 for i := 0; i < len(r.Statuses); i++ { 1778 if visited[i] { 1779 continue 1780 } 1781 if reflect.DeepEqual(expect, r.Statuses[i]) { 1782 visited[i] = true 1783 found = true 1784 break 1785 } 1786 } 1787 if !found { 1788 t.Errorf("did not find status for %v in response", name) 1789 } 1790 } 1791 } 1792 1793 // TestJobService tests that if a job-mode service is created, the necessary 1794 // fields are all initialized to the correct values. Then, it tests that if a 1795 // job-mode service is updated, the fields are updated to correct values. 1796 func TestJobService(t *testing.T) { 1797 ts := newTestServer(t) 1798 defer ts.Stop() 1799 1800 // first, create a replicated job mode service spec 1801 spec := &api.ServiceSpec{ 1802 Annotations: api.Annotations{ 1803 Name: "replicatedjob", 1804 }, 1805 Task: api.TaskSpec{ 1806 Runtime: &api.TaskSpec_Container{ 1807 Container: &api.ContainerSpec{ 1808 Image: "image", 1809 }, 1810 }, 1811 }, 1812 Mode: &api.ServiceSpec_ReplicatedJob{ 1813 ReplicatedJob: &api.ReplicatedJob{ 1814 MaxConcurrent: 3, 1815 TotalCompletions: 9, 1816 }, 1817 }, 1818 } 1819 1820 before := gogotypes.TimestampNow() 1821 // now, create the service 1822 resp, err := ts.Client.CreateService( 1823 context.Background(), &api.CreateServiceRequest{ 1824 Spec: spec, 1825 }, 1826 ) 1827 after := gogotypes.TimestampNow() 1828 1829 // ensure there are no errors 1830 require.NoError(t, err) 1831 // and assert that the response is valid 1832 require.NotNil(t, resp) 1833 require.NotNil(t, resp.Service) 1834 // ensure that the service has a JobStatus set 1835 require.NotNil(t, resp.Service.JobStatus, "expected JobStatus to not be nil") 1836 // and ensure that JobStatus.JobIteration is set to 0, which is the default 1837 require.Equal( 1838 t, resp.Service.JobStatus.JobIteration.Index, uint64(0), 1839 "expected JobIteration for new replicated job to be 0", 1840 ) 1841 require.NotNil(t, resp.Service.JobStatus.LastExecution) 1842 assert.True(t, resp.Service.JobStatus.LastExecution.Compare(before) >= 0, 1843 "expected %v to be after %v", resp.Service.JobStatus.LastExecution, before, 1844 ) 1845 assert.True(t, resp.Service.JobStatus.LastExecution.Compare(after) <= 0, 1846 "expected %v to be before %v", resp.Service.JobStatus.LastExecution, after, 1847 ) 1848 1849 // now, repeat all of the above, but with a global job 1850 gspec := spec.Copy() 1851 gspec.Annotations.Name = "globaljob" 1852 gspec.Mode = &api.ServiceSpec_GlobalJob{ 1853 GlobalJob: &api.GlobalJob{}, 1854 } 1855 before = gogotypes.TimestampNow() 1856 gresp, gerr := ts.Client.CreateService( 1857 context.Background(), &api.CreateServiceRequest{ 1858 Spec: gspec, 1859 }, 1860 ) 1861 after = gogotypes.TimestampNow() 1862 1863 require.NoError(t, gerr) 1864 require.NotNil(t, gresp) 1865 require.NotNil(t, gresp.Service) 1866 require.NotNil(t, gresp.Service.JobStatus) 1867 require.Equal( 1868 t, gresp.Service.JobStatus.JobIteration.Index, uint64(0), 1869 "expected JobIteration for new global job to be 0", 1870 ) 1871 require.NotNil(t, gresp.Service.JobStatus.LastExecution) 1872 assert.True(t, gresp.Service.JobStatus.LastExecution.Compare(before) >= 0, 1873 "expected %v to be after %v", gresp.Service.JobStatus.LastExecution, before, 1874 ) 1875 assert.True(t, gresp.Service.JobStatus.LastExecution.Compare(after) <= 0, 1876 "expected %v to be before %v", gresp.Service.JobStatus.LastExecution, after, 1877 ) 1878 1879 // now test that updating the service increments the JobIteration 1880 spec.Task.ForceUpdate = spec.Task.ForceUpdate + 1 1881 before = gogotypes.TimestampNow() 1882 uresp, uerr := ts.Client.UpdateService( 1883 context.Background(), &api.UpdateServiceRequest{ 1884 ServiceID: resp.Service.ID, 1885 ServiceVersion: &(resp.Service.Meta.Version), 1886 Spec: spec, 1887 }, 1888 ) 1889 after = gogotypes.TimestampNow() 1890 1891 require.NoError(t, uerr) 1892 require.NotNil(t, uresp) 1893 require.NotNil(t, uresp.Service) 1894 require.NotNil(t, uresp.Service.JobStatus) 1895 // updating the service should bump the JobStatus.JobIteration.Index by 1 1896 require.Equal( 1897 t, uresp.Service.JobStatus.JobIteration.Index, uint64(1), 1898 "expected JobIteration for updated replicated job to be 1", 1899 ) 1900 require.NotNil(t, uresp.Service.JobStatus.LastExecution) 1901 assert.True(t, uresp.Service.JobStatus.LastExecution.Compare(before) >= 0, 1902 "expected %v to be after %v", uresp.Service.JobStatus.LastExecution, before, 1903 ) 1904 assert.True(t, uresp.Service.JobStatus.LastExecution.Compare(after) <= 0, 1905 "expected %v to be before %v", uresp.Service.JobStatus.LastExecution, after, 1906 ) 1907 1908 // rinse and repeat 1909 gspec.Task.ForceUpdate = spec.Task.ForceUpdate + 1 1910 before = gogotypes.TimestampNow() 1911 guresp, guerr := ts.Client.UpdateService( 1912 context.Background(), &api.UpdateServiceRequest{ 1913 ServiceID: gresp.Service.ID, 1914 ServiceVersion: &(gresp.Service.Meta.Version), 1915 Spec: gspec, 1916 }, 1917 ) 1918 after = gogotypes.TimestampNow() 1919 1920 require.NoError(t, guerr) 1921 require.NotNil(t, guresp) 1922 require.NotNil(t, guresp.Service) 1923 require.NotNil(t, guresp.Service.JobStatus) 1924 require.Equal( 1925 t, guresp.Service.JobStatus.JobIteration.Index, uint64(1), 1926 "expected JobIteration for updated replicated job to be 1", 1927 ) 1928 require.NotNil(t, guresp.Service.JobStatus.LastExecution) 1929 assert.True(t, guresp.Service.JobStatus.LastExecution.Compare(before) >= 0, 1930 "expected %v to be after %v", guresp.Service.JobStatus.LastExecution, before, 1931 ) 1932 assert.True(t, guresp.Service.JobStatus.LastExecution.Compare(after) <= 0, 1933 "expected %v to be before %v", guresp.Service.JobStatus.LastExecution, after, 1934 ) 1935 } 1936 1937 // TestServiceValidateJob tests that calling the service API correctly 1938 // validates a job-mode service. Some fields, like UpdateConfig, are not valid 1939 // for job-mode services, and should return an error if they're set 1940 func TestServiceValidateJob(t *testing.T) { 1941 bad := &api.ServiceSpec{ 1942 Mode: &api.ServiceSpec_ReplicatedJob{ReplicatedJob: &api.ReplicatedJob{}}, 1943 Update: &api.UpdateConfig{}, 1944 } 1945 1946 err := validateJob(bad) 1947 assert.Error(t, err) 1948 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err)) 1949 1950 good := []*api.ServiceSpec{ 1951 { 1952 Mode: &api.ServiceSpec_ReplicatedJob{ 1953 ReplicatedJob: &api.ReplicatedJob{ 1954 MaxConcurrent: 3, TotalCompletions: 9, 1955 }, 1956 }, 1957 }, 1958 {Mode: &api.ServiceSpec_GlobalJob{}}, 1959 } 1960 1961 for _, g := range good { 1962 err := validateJob(g) 1963 assert.NoError(t, err) 1964 } 1965 }