github.com/click2cloud/libcompose@v0.4.1-0.20170816121048-7c20f79ac6b9/docker/service/service.go (about) 1 package service 2 3 import ( 4 "fmt" 5 "strings" 6 "time" 7 8 "golang.org/x/net/context" 9 10 "github.com/Sirupsen/logrus" 11 "github.com/docker/docker/api/types" 12 "github.com/docker/docker/api/types/filters" 13 "github.com/docker/docker/api/types/network" 14 "github.com/docker/docker/client" 15 "github.com/docker/go-connections/nat" 16 "github.com/Click2Cloud/libcompose/config" 17 "github.com/Click2Cloud/libcompose/docker/auth" 18 "github.com/Click2Cloud/libcompose/docker/builder" 19 composeclient "github.com/Click2Cloud/libcompose/docker/client" 20 "github.com/Click2Cloud/libcompose/docker/container" 21 "github.com/Click2Cloud/libcompose/docker/ctx" 22 "github.com/Click2Cloud/libcompose/docker/image" 23 "github.com/Click2Cloud/libcompose/labels" 24 "github.com/Click2Cloud/libcompose/project" 25 "github.com/Click2Cloud/libcompose/project/events" 26 "github.com/Click2Cloud/libcompose/project/options" 27 "github.com/Click2Cloud/libcompose/utils" 28 "github.com/Click2Cloud/libcompose/yaml" 29 ) 30 31 // Service is a project.Service implementations. 32 type Service struct { 33 name string 34 project *project.Project 35 serviceConfig *config.ServiceConfig 36 clientFactory composeclient.Factory 37 authLookup auth.Lookup 38 39 // FIXME(vdemeester) remove this at some point 40 context *ctx.Context 41 } 42 43 // NewService creates a service 44 func NewService(name string, serviceConfig *config.ServiceConfig, context *ctx.Context) *Service { 45 return &Service{ 46 name: name, 47 project: context.Project, 48 serviceConfig: serviceConfig, 49 clientFactory: context.ClientFactory, 50 authLookup: context.AuthLookup, 51 context: context, 52 } 53 } 54 55 // Name returns the service name. 56 func (s *Service) Name() string { 57 return s.name 58 } 59 60 // Config returns the configuration of the service (config.ServiceConfig). 61 func (s *Service) Config() *config.ServiceConfig { 62 return s.serviceConfig 63 } 64 65 // DependentServices returns the dependent services (as an array of ServiceRelationship) of the service. 66 func (s *Service) DependentServices() []project.ServiceRelationship { 67 return DefaultDependentServices(s.project, s) 68 } 69 70 // Create implements Service.Create. It ensures the image exists or build it 71 // if it can and then create a container. 72 func (s *Service) Create(ctx context.Context, options options.Create) error { 73 containers, err := s.collectContainers(ctx) 74 if err != nil { 75 return err 76 } 77 78 if err := s.ensureImageExists(ctx, options.NoBuild, options.ForceBuild); err != nil { 79 return err 80 } 81 82 if len(containers) != 0 { 83 return s.eachContainer(ctx, containers, func(c *container.Container) error { 84 _, err := s.recreateIfNeeded(ctx, c, options.NoRecreate, options.ForceRecreate) 85 return err 86 }) 87 } 88 89 namer, err := s.namer(ctx, 1) 90 if err != nil { 91 return err 92 } 93 94 _, err = s.createContainer(ctx, namer, "", nil, false) 95 return err 96 } 97 98 func (s *Service) namer(ctx context.Context, count int) (Namer, error) { 99 var namer Namer 100 var err error 101 102 if s.serviceConfig.ContainerName != "" { 103 if count > 1 { 104 logrus.Warnf(`The "%s" service is using the custom container name "%s". Docker requires each container to have a unique name. Remove the custom name to scale the service.`, s.name, s.serviceConfig.ContainerName) 105 } 106 namer = NewSingleNamer(s.serviceConfig.ContainerName) 107 } else { 108 client := s.clientFactory.Create(s) 109 namer, err = NewNamer(ctx, client, s.project.Name, s.name, false) 110 if err != nil { 111 return nil, err 112 } 113 } 114 return namer, nil 115 } 116 117 func (s *Service) collectContainers(ctx context.Context) ([]*container.Container, error) { 118 client := s.clientFactory.Create(s) 119 containers, err := container.ListByFilter(ctx, client, labels.SERVICE.Eq(s.name), labels.PROJECT.Eq(s.project.Name)) 120 if err != nil { 121 return nil, err 122 } 123 124 result := []*container.Container{} 125 126 for _, cont := range containers { 127 c, err := container.New(ctx, client, cont.ID) 128 if err != nil { 129 return nil, err 130 } 131 result = append(result, c) 132 } 133 134 return result, nil 135 } 136 137 func (s *Service) ensureImageExists(ctx context.Context, noBuild bool, forceBuild bool) error { 138 if forceBuild { 139 return s.build(ctx, options.Build{}) 140 } 141 142 exists, err := image.Exists(ctx, s.clientFactory.Create(s), s.imageName()) 143 if err != nil { 144 return err 145 } 146 if exists { 147 return nil 148 } 149 150 if s.Config().Build.Context != "" { 151 if noBuild { 152 return fmt.Errorf("Service %q needs to be built, but no-build was specified", s.name) 153 } 154 return s.build(ctx, options.Build{}) 155 } 156 157 return s.Pull(ctx) 158 } 159 160 func (s *Service) imageName() string { 161 if s.Config().Image != "" { 162 return s.Config().Image 163 } 164 return fmt.Sprintf("%s_%s", s.project.Name, s.Name()) 165 } 166 167 // Build implements Service.Build. It will try to build the image and returns an error if any. 168 func (s *Service) Build(ctx context.Context, buildOptions options.Build) error { 169 return s.build(ctx, buildOptions) 170 } 171 172 func (s *Service) build(ctx context.Context, buildOptions options.Build) error { 173 if s.Config().Build.Context == "" { 174 return fmt.Errorf("Specified service does not have a build section") 175 } 176 builder := &builder.DaemonBuilder{ 177 Client: s.clientFactory.Create(s), 178 ContextDirectory: s.Config().Build.Context, 179 Dockerfile: s.Config().Build.Dockerfile, 180 BuildArgs: s.Config().Build.Args, 181 AuthConfigs: s.authLookup.All(), 182 NoCache: buildOptions.NoCache, 183 ForceRemove: buildOptions.ForceRemove, 184 Pull: buildOptions.Pull, 185 LoggerFactory: s.context.LoggerFactory, 186 } 187 return builder.Build(ctx, s.imageName()) 188 } 189 190 func (s *Service) constructContainers(ctx context.Context, count int) ([]*container.Container, error) { 191 result, err := s.collectContainers(ctx) 192 if err != nil { 193 return nil, err 194 } 195 196 client := s.clientFactory.Create(s) 197 198 var namer Namer 199 200 if s.serviceConfig.ContainerName != "" { 201 if count > 1 { 202 logrus.Warnf(`The "%s" service is using the custom container name "%s". Docker requires each container to have a unique name. Remove the custom name to scale the service.`, s.name, s.serviceConfig.ContainerName) 203 } 204 namer = NewSingleNamer(s.serviceConfig.ContainerName) 205 } else { 206 namer, err = NewNamer(ctx, client, s.project.Name, s.name, false) 207 if err != nil { 208 return nil, err 209 } 210 } 211 212 for i := len(result); i < count; i++ { 213 c, err := s.createContainer(ctx, namer, "", nil, false) 214 if err != nil { 215 return nil, err 216 } 217 218 id := c.ID() 219 logrus.Debugf("Created container %s: %v", id, c.Name()) 220 221 result = append(result, c) 222 } 223 224 return result, nil 225 } 226 227 // Up implements Service.Up. It builds the image if needed, creates a container 228 // and start it. 229 func (s *Service) Up(ctx context.Context, options options.Up) error { 230 containers, err := s.collectContainers(ctx) 231 if err != nil { 232 return err 233 } 234 235 var imageName = s.imageName() 236 if len(containers) == 0 || !options.NoRecreate { 237 if err = s.ensureImageExists(ctx, options.NoBuild, options.ForceBuild); err != nil { 238 return err 239 } 240 } 241 242 return s.up(ctx, imageName, true, options) 243 } 244 245 // Run implements Service.Run. It runs a one of command within the service container. 246 // It always create a new container. 247 func (s *Service) Run(ctx context.Context, commandParts []string, options options.Run) (int, error) { 248 err := s.ensureImageExists(ctx, false, false) 249 if err != nil { 250 return -1, err 251 } 252 253 client := s.clientFactory.Create(s) 254 255 namer, err := NewNamer(ctx, client, s.project.Name, s.name, true) 256 if err != nil { 257 return -1, err 258 } 259 260 configOverride := &config.ServiceConfig{Command: commandParts, Tty: true, StdinOpen: true} 261 262 c, err := s.createContainer(ctx, namer, "", configOverride, true) 263 if err != nil { 264 return -1, err 265 } 266 267 if err := s.connectContainerToNetworks(ctx, c, true); err != nil { 268 return -1, err 269 } 270 271 if options.Detached { 272 logrus.Infof("%s", c.Name()) 273 return 0, c.Start(ctx) 274 } 275 return c.Run(ctx, configOverride) 276 } 277 278 // Info implements Service.Info. It returns an project.InfoSet with the containers 279 // related to this service (can be multiple if using the scale command). 280 func (s *Service) Info(ctx context.Context) (project.InfoSet, error) { 281 result := project.InfoSet{} 282 containers, err := s.collectContainers(ctx) 283 if err != nil { 284 return nil, err 285 } 286 287 for _, c := range containers { 288 info, err := c.Info(ctx) 289 if err != nil { 290 return nil, err 291 } 292 result = append(result, info) 293 } 294 295 return result, nil 296 } 297 298 // Start implements Service.Start. It tries to start a container without creating it. 299 func (s *Service) Start(ctx context.Context) error { 300 return s.collectContainersAndDo(ctx, func(c *container.Container) error { 301 if err := s.connectContainerToNetworks(ctx, c, false); err != nil { 302 return err 303 } 304 return c.Start(ctx) 305 }) 306 } 307 308 func (s *Service) up(ctx context.Context, imageName string, create bool, options options.Up) error { 309 containers, err := s.collectContainers(ctx) 310 if err != nil { 311 return err 312 } 313 314 logrus.Debugf("Found %d existing containers for service %s", len(containers), s.name) 315 316 if len(containers) == 0 && create { 317 namer, err := s.namer(ctx, 1) 318 if err != nil { 319 return err 320 } 321 c, err := s.createContainer(ctx, namer, "", nil, false) 322 if err != nil { 323 return err 324 } 325 containers = []*container.Container{c} 326 } 327 328 return s.eachContainer(ctx, containers, func(c *container.Container) error { 329 var err error 330 if create { 331 c, err = s.recreateIfNeeded(ctx, c, options.NoRecreate, options.ForceRecreate) 332 if err != nil { 333 return err 334 } 335 } 336 337 if err := s.connectContainerToNetworks(ctx, c, false); err != nil { 338 return err 339 } 340 341 err = c.Start(ctx) 342 343 if err == nil { 344 s.project.Notify(events.ContainerStarted, s.name, map[string]string{ 345 "name": c.Name(), 346 }) 347 } 348 349 return err 350 }) 351 } 352 353 func (s *Service) connectContainerToNetworks(ctx context.Context, c *container.Container, oneOff bool) error { 354 connectedNetworks, err := c.Networks() 355 if err != nil { 356 return nil 357 } 358 if s.serviceConfig.Networks != nil { 359 for _, network := range s.serviceConfig.Networks.Networks { 360 existingNetwork, ok := connectedNetworks[network.Name] 361 if ok { 362 // FIXME(vdemeester) implement alias checking (to not disconnect/reconnect for nothing) 363 aliasPresent := false 364 for _, alias := range existingNetwork.Aliases { 365 ID := c.ShortID() 366 if alias == ID { 367 aliasPresent = true 368 } 369 } 370 if aliasPresent { 371 continue 372 } 373 if err := s.NetworkDisconnect(ctx, c, network, oneOff); err != nil { 374 return err 375 } 376 } 377 if err := s.NetworkConnect(ctx, c, network, oneOff); err != nil { 378 return err 379 } 380 } 381 } 382 return nil 383 } 384 385 // NetworkDisconnect disconnects the container from the specified network 386 func (s *Service) NetworkDisconnect(ctx context.Context, c *container.Container, net *yaml.Network, oneOff bool) error { 387 containerID := c.ID() 388 client := s.clientFactory.Create(s) 389 return client.NetworkDisconnect(ctx, net.RealName, containerID, true) 390 } 391 392 // NetworkConnect connects the container to the specified network 393 // FIXME(vdemeester) will be refactor with Container refactoring 394 func (s *Service) NetworkConnect(ctx context.Context, c *container.Container, net *yaml.Network, oneOff bool) error { 395 containerID := c.ID() 396 client := s.clientFactory.Create(s) 397 internalLinks, err := s.getLinks() 398 if err != nil { 399 return err 400 } 401 links := []string{} 402 // TODO(vdemeester) handle link to self (?) 403 for k, v := range internalLinks { 404 links = append(links, strings.Join([]string{v, k}, ":")) 405 } 406 for _, v := range s.serviceConfig.ExternalLinks { 407 links = append(links, v) 408 } 409 aliases := []string{} 410 if !oneOff { 411 aliases = []string{s.Name()} 412 } 413 aliases = append(aliases, net.Aliases...) 414 return client.NetworkConnect(ctx, net.RealName, containerID, &network.EndpointSettings{ 415 Aliases: aliases, 416 Links: links, 417 IPAddress: net.IPv4Address, 418 IPAMConfig: &network.EndpointIPAMConfig{ 419 IPv4Address: net.IPv4Address, 420 IPv6Address: net.IPv6Address, 421 }, 422 }) 423 } 424 425 func (s *Service) recreateIfNeeded(ctx context.Context, c *container.Container, noRecreate, forceRecreate bool) (*container.Container, error) { 426 if noRecreate { 427 return c, nil 428 } 429 outOfSync, err := s.OutOfSync(ctx, c) 430 if err != nil { 431 return c, err 432 } 433 434 logrus.WithFields(logrus.Fields{ 435 "outOfSync": outOfSync, 436 "ForceRecreate": forceRecreate, 437 "NoRecreate": noRecreate}).Debug("Going to decide if recreate is needed") 438 439 if forceRecreate || outOfSync { 440 logrus.Infof("Recreating %s", s.name) 441 newContainer, err := s.recreate(ctx, c) 442 if err != nil { 443 return c, err 444 } 445 return newContainer, nil 446 } 447 448 return c, err 449 } 450 451 func (s *Service) recreate(ctx context.Context, c *container.Container) (*container.Container, error) { 452 name := c.Name() 453 id := c.ID() 454 newName := fmt.Sprintf("%s_%s", name, id[:12]) 455 logrus.Debugf("Renaming %s => %s", name, newName) 456 if err := c.Rename(ctx, newName); err != nil { 457 logrus.Errorf("Failed to rename old container %s", c.Name()) 458 return nil, err 459 } 460 namer := NewSingleNamer(name) 461 newContainer, err := s.createContainer(ctx, namer, id, nil, false) 462 if err != nil { 463 return nil, err 464 } 465 newID := newContainer.ID() 466 logrus.Debugf("Created replacement container %s", newID) 467 if err := c.Remove(ctx, false); err != nil { 468 logrus.Errorf("Failed to remove old container %s", c.Name()) 469 return nil, err 470 } 471 logrus.Debugf("Removed old container %s %s", c.Name(), id) 472 return newContainer, nil 473 } 474 475 // OutOfSync checks if the container is out of sync with the service definition. 476 // It looks if the the service hash container label is the same as the computed one. 477 func (s *Service) OutOfSync(ctx context.Context, c *container.Container) (bool, error) { 478 if c.ImageConfig() != s.serviceConfig.Image { 479 logrus.Debugf("Images for %s do not match %s!=%s", c.Name(), c.ImageConfig(), s.serviceConfig.Image) 480 return true, nil 481 } 482 483 expectedHash := config.GetServiceHash(s.name, s.Config()) 484 if c.Hash() != expectedHash { 485 logrus.Debugf("Hashes for %s do not match %s!=%s", c.Name(), c.Hash(), expectedHash) 486 return true, nil 487 } 488 489 image, err := image.InspectImage(ctx, s.clientFactory.Create(s), c.ImageConfig()) 490 if err != nil { 491 if client.IsErrImageNotFound(err) { 492 logrus.Debugf("Image %s do not exist, do not know if it's out of sync", c.Image()) 493 return false, nil 494 } 495 return false, err 496 } 497 498 logrus.Debugf("Checking existing image name vs id: %s == %s", image.ID, c.Image()) 499 return image.ID != c.Image(), err 500 } 501 502 func (s *Service) collectContainersAndDo(ctx context.Context, action func(*container.Container) error) error { 503 containers, err := s.collectContainers(ctx) 504 if err != nil { 505 return err 506 } 507 return s.eachContainer(ctx, containers, action) 508 } 509 510 func (s *Service) eachContainer(ctx context.Context, containers []*container.Container, action func(*container.Container) error) error { 511 512 tasks := utils.InParallel{} 513 for _, cont := range containers { 514 task := func(cont *container.Container) func() error { 515 return func() error { 516 return action(cont) 517 } 518 }(cont) 519 520 tasks.Add(task) 521 } 522 523 return tasks.Wait() 524 } 525 526 // Stop implements Service.Stop. It stops any containers related to the service. 527 func (s *Service) Stop(ctx context.Context, timeout int) error { 528 timeout = s.stopTimeout(timeout) 529 return s.collectContainersAndDo(ctx, func(c *container.Container) error { 530 return c.Stop(ctx, timeout) 531 }) 532 } 533 534 // Restart implements Service.Restart. It restarts any containers related to the service. 535 func (s *Service) Restart(ctx context.Context, timeout int) error { 536 timeout = s.stopTimeout(timeout) 537 return s.collectContainersAndDo(ctx, func(c *container.Container) error { 538 return c.Restart(ctx, timeout) 539 }) 540 } 541 542 // Kill implements Service.Kill. It kills any containers related to the service. 543 func (s *Service) Kill(ctx context.Context, signal string) error { 544 return s.collectContainersAndDo(ctx, func(c *container.Container) error { 545 return c.Kill(ctx, signal) 546 }) 547 } 548 549 // Delete implements Service.Delete. It removes any containers related to the service. 550 func (s *Service) Delete(ctx context.Context, options options.Delete) error { 551 return s.collectContainersAndDo(ctx, func(c *container.Container) error { 552 running := c.IsRunning(ctx) 553 if !running || options.RemoveRunning { 554 return c.Remove(ctx, options.RemoveVolume) 555 } 556 return nil 557 }) 558 } 559 560 // Log implements Service.Log. It returns the docker logs for each container related to the service. 561 func (s *Service) Log(ctx context.Context, follow bool) error { 562 return s.collectContainersAndDo(ctx, func(c *container.Container) error { 563 containerNumber, err := c.Number() 564 if err != nil { 565 return err 566 } 567 name := fmt.Sprintf("%s_%d", s.name, containerNumber) 568 if s.Config().ContainerName != "" { 569 name = s.Config().ContainerName 570 } 571 l := s.context.LoggerFactory.CreateContainerLogger(name) 572 return c.Log(ctx, l, follow) 573 }) 574 } 575 576 // Scale implements Service.Scale. It creates or removes containers to have the specified number 577 // of related container to the service to run. 578 func (s *Service) Scale(ctx context.Context, scale int, timeout int) error { 579 if s.specificiesHostPort() { 580 logrus.Warnf("The \"%s\" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.", s.Name()) 581 } 582 583 containers, err := s.collectContainers(ctx) 584 if err != nil { 585 return err 586 } 587 if len(containers) > scale { 588 foundCount := 0 589 for _, c := range containers { 590 foundCount++ 591 if foundCount > scale { 592 timeout = s.stopTimeout(timeout) 593 if err := c.Stop(ctx, timeout); err != nil { 594 return err 595 } 596 // FIXME(vdemeester) remove volume in scale by default ? 597 if err := c.Remove(ctx, false); err != nil { 598 return err 599 } 600 } 601 } 602 } 603 604 if err != nil { 605 return err 606 } 607 608 if len(containers) < scale { 609 err := s.ensureImageExists(ctx, false, false) 610 if err != nil { 611 return err 612 } 613 614 if _, err = s.constructContainers(ctx, scale); err != nil { 615 return err 616 } 617 } 618 619 return s.up(ctx, "", false, options.Up{}) 620 } 621 622 // Pull implements Service.Pull. It pulls the image of the service and skip the service that 623 // would need to be built. 624 func (s *Service) Pull(ctx context.Context) error { 625 if s.Config().Image == "" { 626 return nil 627 } 628 629 return image.PullImage(ctx, s.clientFactory.Create(s), s.name, s.authLookup, s.Config().Image) 630 } 631 632 // Pause implements Service.Pause. It puts into pause the container(s) related 633 // to the service. 634 func (s *Service) Pause(ctx context.Context) error { 635 return s.collectContainersAndDo(ctx, func(c *container.Container) error { 636 return c.Pause(ctx) 637 }) 638 } 639 640 // Unpause implements Service.Pause. It brings back from pause the container(s) 641 // related to the service. 642 func (s *Service) Unpause(ctx context.Context) error { 643 return s.collectContainersAndDo(ctx, func(c *container.Container) error { 644 return c.Unpause(ctx) 645 }) 646 } 647 648 // RemoveImage implements Service.RemoveImage. It removes images used for the service 649 // depending on the specified type. 650 func (s *Service) RemoveImage(ctx context.Context, imageType options.ImageType) error { 651 switch imageType { 652 case "local": 653 if s.Config().Image != "" { 654 return nil 655 } 656 return image.RemoveImage(ctx, s.clientFactory.Create(s), s.imageName()) 657 case "all": 658 return image.RemoveImage(ctx, s.clientFactory.Create(s), s.imageName()) 659 default: 660 // Don't do a thing, should be validated up-front 661 return nil 662 } 663 } 664 665 var eventAttributes = []string{"image", "name"} 666 667 // Events implements Service.Events. It listen to all real-time events happening 668 // for the service, and put them into the specified chan. 669 func (s *Service) Events(ctx context.Context, evts chan events.ContainerEvent) error { 670 filter := filters.NewArgs() 671 filter.Add("label", fmt.Sprintf("%s=%s", labels.PROJECT, s.project.Name)) 672 filter.Add("label", fmt.Sprintf("%s=%s", labels.SERVICE, s.name)) 673 client := s.clientFactory.Create(s) 674 eventq, errq := client.Events(ctx, types.EventsOptions{ 675 Filters: filter, 676 }) 677 go func() { 678 for { 679 select { 680 case event := <-eventq: 681 service := event.Actor.Attributes[labels.SERVICE.Str()] 682 attributes := map[string]string{} 683 for _, attr := range eventAttributes { 684 attributes[attr] = event.Actor.Attributes[attr] 685 } 686 e := events.ContainerEvent{ 687 Service: service, 688 Event: event.Action, 689 Type: event.Type, 690 ID: event.Actor.ID, 691 Time: time.Unix(event.Time, 0), 692 Attributes: attributes, 693 } 694 evts <- e 695 } 696 } 697 }() 698 return <-errq 699 } 700 701 // Containers implements Service.Containers. It returns the list of containers 702 // that are related to the service. 703 func (s *Service) Containers(ctx context.Context) ([]project.Container, error) { 704 result := []project.Container{} 705 containers, err := s.collectContainers(ctx) 706 if err != nil { 707 return nil, err 708 } 709 710 for _, c := range containers { 711 result = append(result, c) 712 } 713 714 return result, nil 715 } 716 717 func (s *Service) specificiesHostPort() bool { 718 _, bindings, err := nat.ParsePortSpecs(s.Config().Ports) 719 720 if err != nil { 721 fmt.Println(err) 722 } 723 724 for _, portBindings := range bindings { 725 for _, portBinding := range portBindings { 726 if portBinding.HostPort != "" { 727 return true 728 } 729 } 730 } 731 732 return false 733 } 734 735 //take in timeout flag from cli as parameter 736 //return timeout if it is set, 737 //else return stop_grace_period if it is set, 738 //else return default 10s 739 func (s *Service) stopTimeout(timeout int) int { 740 DEFAULTTIMEOUT := 10 741 if timeout != 0 { 742 return timeout 743 } 744 configTimeout := utils.DurationStrToSecondsInt(s.Config().StopGracePeriod) 745 if configTimeout != nil { 746 return *configTimeout 747 } 748 return DEFAULTTIMEOUT 749 }