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