github.com/skippbox/kompose-origin@v0.0.0-20160524133224-16a9dca7bac2/project/project.go (about) 1 package project 2 3 import ( 4 "errors" 5 "fmt" 6 "strings" 7 8 "golang.org/x/net/context" 9 10 log "github.com/Sirupsen/logrus" 11 "github.com/docker/engine-api/types" 12 "github.com/docker/engine-api/types/filters" 13 "github.com/docker/libcompose/config" 14 "github.com/docker/libcompose/labels" 15 "github.com/docker/libcompose/logger" 16 "github.com/docker/libcompose/project/events" 17 "github.com/docker/libcompose/project/options" 18 "github.com/docker/libcompose/utils" 19 ) 20 21 type wrapperAction func(*serviceWrapper, map[string]*serviceWrapper) 22 type serviceAction func(service Service) error 23 24 // Project holds libcompose project information. 25 type Project struct { 26 Name string 27 ServiceConfigs *config.ServiceConfigs 28 VolumeConfigs map[string]*config.VolumeConfig 29 NetworkConfigs map[string]*config.NetworkConfig 30 Files []string 31 ReloadCallback func() error 32 ParseOptions *config.ParseOptions 33 34 context *Context 35 clientFactory ClientFactory 36 reload []string 37 upCount int 38 listeners []chan<- events.Event 39 hasListeners bool 40 } 41 42 // NewProject creates a new project with the specified context. 43 func NewProject(clientFactory ClientFactory, context *Context, parseOptions *config.ParseOptions) *Project { 44 p := &Project{ 45 context: context, 46 clientFactory: clientFactory, 47 ParseOptions: parseOptions, 48 ServiceConfigs: config.NewServiceConfigs(), 49 VolumeConfigs: make(map[string]*config.VolumeConfig), 50 NetworkConfigs: make(map[string]*config.NetworkConfig), 51 } 52 53 if context.LoggerFactory == nil { 54 context.LoggerFactory = &logger.NullLogger{} 55 } 56 57 context.Project = p 58 59 p.listeners = []chan<- events.Event{NewDefaultListener(p)} 60 61 return p 62 } 63 64 // Parse populates project information based on its context. It sets up the name, 65 // the composefile and the composebytes (the composefile content). 66 func (p *Project) Parse() error { 67 err := p.context.open() 68 if err != nil { 69 return err 70 } 71 72 p.Name = p.context.ProjectName 73 74 p.Files = p.context.ComposeFiles 75 76 if len(p.Files) == 1 && p.Files[0] == "-" { 77 p.Files = []string{"."} 78 } 79 80 if p.context.ComposeBytes != nil { 81 for i, composeBytes := range p.context.ComposeBytes { 82 file := "" 83 if i < len(p.context.ComposeFiles) { 84 file = p.Files[i] 85 } 86 if err := p.load(file, composeBytes); err != nil { 87 return err 88 } 89 } 90 } 91 92 return nil 93 } 94 95 // CreateService creates a service with the specified name based. If there 96 // is no config in the project for this service, it will return an error. 97 func (p *Project) CreateService(name string) (Service, error) { 98 existing, ok := p.ServiceConfigs.Get(name) 99 if !ok { 100 return nil, fmt.Errorf("Failed to find service: %s", name) 101 } 102 103 // Copy because we are about to modify the environment 104 config := *existing 105 106 if p.context.EnvironmentLookup != nil { 107 parsedEnv := make([]string, 0, len(config.Environment)) 108 109 for _, env := range config.Environment { 110 parts := strings.SplitN(env, "=", 2) 111 if len(parts) > 1 && parts[1] != "" { 112 parsedEnv = append(parsedEnv, env) 113 continue 114 } else { 115 env = parts[0] 116 } 117 118 for _, value := range p.context.EnvironmentLookup.Lookup(env, name, &config) { 119 parsedEnv = append(parsedEnv, value) 120 } 121 } 122 123 config.Environment = parsedEnv 124 } 125 126 return p.context.ServiceFactory.Create(p, name, &config) 127 } 128 129 // AddConfig adds the specified service config for the specified name. 130 func (p *Project) AddConfig(name string, config *config.ServiceConfig) error { 131 p.Notify(events.ServiceAdd, name, nil) 132 133 p.ServiceConfigs.Add(name, config) 134 p.reload = append(p.reload, name) 135 136 return nil 137 } 138 139 // AddVolumeConfig adds the specified volume config for the specified name. 140 func (p *Project) AddVolumeConfig(name string, config *config.VolumeConfig) error { 141 p.Notify(events.VolumeAdd, name, nil) 142 p.VolumeConfigs[name] = config 143 return nil 144 } 145 146 // AddNetworkConfig adds the specified network config for the specified name. 147 func (p *Project) AddNetworkConfig(name string, config *config.NetworkConfig) error { 148 p.Notify(events.NetworkAdd, name, nil) 149 p.NetworkConfigs[name] = config 150 return nil 151 } 152 153 // Load loads the specified byte array (the composefile content) and adds the 154 // service configuration to the project. 155 // FIXME is it needed ? 156 func (p *Project) Load(bytes []byte) error { 157 return p.load("", bytes) 158 } 159 160 func (p *Project) load(file string, bytes []byte) error { 161 serviceConfigs, volumeConfigs, networkConfigs, err := config.Merge(p.ServiceConfigs, p.context.EnvironmentLookup, p.context.ResourceLookup, file, bytes, p.ParseOptions) 162 if err != nil { 163 log.Errorf("Could not parse config for project %s : %v", p.Name, err) 164 return err 165 } 166 167 for name, config := range serviceConfigs { 168 err := p.AddConfig(name, config) 169 if err != nil { 170 return err 171 } 172 } 173 174 for name, config := range volumeConfigs { 175 err := p.AddVolumeConfig(name, config) 176 if err != nil { 177 return err 178 } 179 } 180 181 for name, config := range networkConfigs { 182 err := p.AddNetworkConfig(name, config) 183 if err != nil { 184 return err 185 } 186 } 187 188 return nil 189 } 190 191 func (p *Project) loadWrappers(wrappers map[string]*serviceWrapper, servicesToConstruct []string) error { 192 for _, name := range servicesToConstruct { 193 wrapper, err := newServiceWrapper(name, p) 194 if err != nil { 195 return err 196 } 197 wrappers[name] = wrapper 198 } 199 200 return nil 201 } 202 203 // Build builds the specified services (like docker build). 204 func (p *Project) Build(ctx context.Context, buildOptions options.Build, services ...string) error { 205 return p.perform(events.ProjectBuildStart, events.ProjectBuildDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 206 wrapper.Do(wrappers, events.ServiceBuildStart, events.ServiceBuild, func(service Service) error { 207 return service.Build(ctx, buildOptions) 208 }) 209 }), nil) 210 } 211 212 // Create creates the specified services (like docker create). 213 func (p *Project) Create(ctx context.Context, options options.Create, services ...string) error { 214 if options.NoRecreate && options.ForceRecreate { 215 return fmt.Errorf("no-recreate and force-recreate cannot be combined") 216 } 217 return p.perform(events.ProjectCreateStart, events.ProjectCreateDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 218 wrapper.Do(wrappers, events.ServiceCreateStart, events.ServiceCreate, func(service Service) error { 219 return service.Create(ctx, options) 220 }) 221 }), nil) 222 } 223 224 // Stop stops the specified services (like docker stop). 225 func (p *Project) Stop(ctx context.Context, timeout int, services ...string) error { 226 return p.perform(events.ProjectStopStart, events.ProjectStopDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 227 wrapper.Do(nil, events.ServiceStopStart, events.ServiceStop, func(service Service) error { 228 return service.Stop(ctx, timeout) 229 }) 230 }), nil) 231 } 232 233 // Down stops the specified services and clean related containers (like docker stop + docker rm). 234 func (p *Project) Down(ctx context.Context, opts options.Down, services ...string) error { 235 if !opts.RemoveImages.Valid() { 236 return fmt.Errorf("--rmi flag must be local, all or empty") 237 } 238 if err := p.Stop(ctx, 10, services...); err != nil { 239 return err 240 } 241 if opts.RemoveOrphans { 242 if err := p.removeOrphanContainers(); err != nil { 243 return err 244 } 245 } 246 if err := p.Delete(ctx, options.Delete{ 247 RemoveVolume: opts.RemoveVolume, 248 }, services...); err != nil { 249 return err 250 } 251 252 return p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 253 wrapper.Do(wrappers, events.NoEvent, events.NoEvent, func(service Service) error { 254 return service.RemoveImage(ctx, opts.RemoveImages) 255 }) 256 }), func(service Service) error { 257 return service.Create(ctx, options.Create{}) 258 }) 259 } 260 261 func (p *Project) removeOrphanContainers() error { 262 client := p.clientFactory.Create(nil) 263 filter := filters.NewArgs() 264 filter.Add("label", labels.PROJECT.EqString(p.Name)) 265 containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{ 266 Filter: filter, 267 }) 268 if err != nil { 269 return err 270 } 271 currentServices := map[string]struct{}{} 272 for _, serviceName := range p.ServiceConfigs.Keys() { 273 currentServices[serviceName] = struct{}{} 274 } 275 for _, container := range containers { 276 serviceLabel := container.Labels[labels.SERVICE.Str()] 277 if _, ok := currentServices[serviceLabel]; !ok { 278 if err := client.ContainerKill(context.Background(), container.ID, "SIGKILL"); err != nil { 279 return err 280 } 281 if err := client.ContainerRemove(context.Background(), container.ID, types.ContainerRemoveOptions{ 282 Force: true, 283 }); err != nil { 284 return err 285 } 286 } 287 } 288 return nil 289 } 290 291 // Restart restarts the specified services (like docker restart). 292 func (p *Project) Restart(ctx context.Context, timeout int, services ...string) error { 293 return p.perform(events.ProjectRestartStart, events.ProjectRestartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 294 wrapper.Do(wrappers, events.ServiceRestartStart, events.ServiceRestart, func(service Service) error { 295 return service.Restart(ctx, timeout) 296 }) 297 }), nil) 298 } 299 300 // Port returns the public port for a port binding of the specified service. 301 func (p *Project) Port(ctx context.Context, index int, protocol, serviceName, privatePort string) (string, error) { 302 service, err := p.CreateService(serviceName) 303 if err != nil { 304 return "", err 305 } 306 307 containers, err := service.Containers(ctx) 308 if err != nil { 309 return "", err 310 } 311 312 if index < 1 || index > len(containers) { 313 return "", fmt.Errorf("Invalid index %d", index) 314 } 315 316 return containers[index-1].Port(ctx, fmt.Sprintf("%s/%s", privatePort, protocol)) 317 } 318 319 // Ps list containers for the specified services. 320 func (p *Project) Ps(ctx context.Context, onlyID bool, services ...string) (InfoSet, error) { 321 allInfo := InfoSet{} 322 for _, name := range p.ServiceConfigs.Keys() { 323 service, err := p.CreateService(name) 324 if err != nil { 325 return nil, err 326 } 327 328 info, err := service.Info(ctx, onlyID) 329 if err != nil { 330 return nil, err 331 } 332 333 allInfo = append(allInfo, info...) 334 } 335 return allInfo, nil 336 } 337 338 // Start starts the specified services (like docker start). 339 func (p *Project) Start(ctx context.Context, services ...string) error { 340 return p.perform(events.ProjectStartStart, events.ProjectStartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 341 wrapper.Do(wrappers, events.ServiceStartStart, events.ServiceStart, func(service Service) error { 342 return service.Start(ctx) 343 }) 344 }), nil) 345 } 346 347 // Run executes a one off command (like `docker run image command`). 348 func (p *Project) Run(ctx context.Context, serviceName string, commandParts []string) (int, error) { 349 if !p.ServiceConfigs.Has(serviceName) { 350 return 1, fmt.Errorf("%s is not defined in the template", serviceName) 351 } 352 353 var exitCode int 354 err := p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 355 wrapper.Do(wrappers, events.ServiceRunStart, events.ServiceRun, func(service Service) error { 356 if service.Name() == serviceName { 357 code, err := service.Run(ctx, commandParts) 358 exitCode = code 359 return err 360 } 361 return nil 362 }) 363 }), func(service Service) error { 364 return service.Create(ctx, options.Create{}) 365 }) 366 return exitCode, err 367 } 368 369 // Up creates and starts the specified services (kinda like docker run). 370 func (p *Project) Up(ctx context.Context, options options.Up, services ...string) error { 371 return p.perform(events.ProjectUpStart, events.ProjectUpDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 372 wrapper.Do(wrappers, events.ServiceUpStart, events.ServiceUp, func(service Service) error { 373 return service.Up(ctx, options) 374 }) 375 }), func(service Service) error { 376 return service.Create(ctx, options.Create) 377 }) 378 } 379 380 // Log aggregates and prints out the logs for the specified services. 381 func (p *Project) Log(ctx context.Context, follow bool, services ...string) error { 382 return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 383 wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error { 384 return service.Log(ctx, follow) 385 }) 386 }), nil) 387 } 388 389 // Scale scales the specified services. 390 func (p *Project) Scale(ctx context.Context, timeout int, servicesScale map[string]int) error { 391 // This code is a bit verbose but I wanted to parse everything up front 392 order := make([]string, 0, 0) 393 services := make(map[string]Service) 394 395 for name := range servicesScale { 396 if !p.ServiceConfigs.Has(name) { 397 return fmt.Errorf("%s is not defined in the template", name) 398 } 399 400 service, err := p.CreateService(name) 401 if err != nil { 402 return fmt.Errorf("Failed to lookup service: %s: %v", service, err) 403 } 404 405 order = append(order, name) 406 services[name] = service 407 } 408 409 for _, name := range order { 410 scale := servicesScale[name] 411 log.Infof("Setting scale %s=%d...", name, scale) 412 err := services[name].Scale(ctx, scale, timeout) 413 if err != nil { 414 return fmt.Errorf("Failed to set the scale %s=%d: %v", name, scale, err) 415 } 416 } 417 return nil 418 } 419 420 // Pull pulls the specified services (like docker pull). 421 func (p *Project) Pull(ctx context.Context, services ...string) error { 422 return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 423 wrapper.Do(nil, events.ServicePullStart, events.ServicePull, func(service Service) error { 424 return service.Pull(ctx) 425 }) 426 }), nil) 427 } 428 429 // listStoppedContainers lists the stopped containers for the specified services. 430 func (p *Project) listStoppedContainers(ctx context.Context, services ...string) ([]string, error) { 431 stoppedContainers := []string{} 432 err := p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 433 wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error { 434 containers, innerErr := service.Containers(ctx) 435 if innerErr != nil { 436 return innerErr 437 } 438 439 for _, container := range containers { 440 running, innerErr := container.IsRunning(ctx) 441 if innerErr != nil { 442 log.Error(innerErr) 443 } 444 if !running { 445 containerID, innerErr := container.ID() 446 if innerErr != nil { 447 log.Error(innerErr) 448 } 449 stoppedContainers = append(stoppedContainers, containerID) 450 } 451 } 452 453 return nil 454 }) 455 }), nil) 456 if err != nil { 457 return nil, err 458 } 459 return stoppedContainers, nil 460 } 461 462 // Delete removes the specified services (like docker rm). 463 func (p *Project) Delete(ctx context.Context, options options.Delete, services ...string) error { 464 stoppedContainers, err := p.listStoppedContainers(ctx, services...) 465 if err != nil { 466 return err 467 } 468 if len(stoppedContainers) == 0 { 469 p.Notify(events.ProjectDeleteDone, "", nil) 470 fmt.Println("No stopped containers") 471 return nil 472 } 473 if options.BeforeDeleteCallback != nil && !options.BeforeDeleteCallback(stoppedContainers) { 474 return nil 475 } 476 return p.perform(events.ProjectDeleteStart, events.ProjectDeleteDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 477 wrapper.Do(nil, events.ServiceDeleteStart, events.ServiceDelete, func(service Service) error { 478 return service.Delete(ctx, options) 479 }) 480 }), nil) 481 } 482 483 // Kill kills the specified services (like docker kill). 484 func (p *Project) Kill(ctx context.Context, signal string, services ...string) error { 485 return p.perform(events.ProjectKillStart, events.ProjectKillDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 486 wrapper.Do(nil, events.ServiceKillStart, events.ServiceKill, func(service Service) error { 487 return service.Kill(ctx, signal) 488 }) 489 }), nil) 490 } 491 492 // Pause pauses the specified services containers (like docker pause). 493 func (p *Project) Pause(ctx context.Context, services ...string) error { 494 return p.perform(events.ProjectPauseStart, events.ProjectPauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 495 wrapper.Do(nil, events.ServicePauseStart, events.ServicePause, func(service Service) error { 496 return service.Pause(ctx) 497 }) 498 }), nil) 499 } 500 501 // Unpause pauses the specified services containers (like docker pause). 502 func (p *Project) Unpause(ctx context.Context, services ...string) error { 503 return p.perform(events.ProjectUnpauseStart, events.ProjectUnpauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { 504 wrapper.Do(nil, events.ServiceUnpauseStart, events.ServiceUnpause, func(service Service) error { 505 return service.Unpause(ctx) 506 }) 507 }), nil) 508 } 509 510 func (p *Project) perform(start, done events.EventType, services []string, action wrapperAction, cycleAction serviceAction) error { 511 p.Notify(start, "", nil) 512 513 err := p.forEach(services, action, cycleAction) 514 515 p.Notify(done, "", nil) 516 return err 517 } 518 519 func isSelected(wrapper *serviceWrapper, selected map[string]bool) bool { 520 return len(selected) == 0 || selected[wrapper.name] 521 } 522 523 func (p *Project) forEach(services []string, action wrapperAction, cycleAction serviceAction) error { 524 selected := make(map[string]bool) 525 wrappers := make(map[string]*serviceWrapper) 526 527 for _, s := range services { 528 selected[s] = true 529 } 530 531 return p.traverse(true, selected, wrappers, action, cycleAction) 532 } 533 534 func (p *Project) startService(wrappers map[string]*serviceWrapper, history []string, selected, launched map[string]bool, wrapper *serviceWrapper, action wrapperAction, cycleAction serviceAction) error { 535 if launched[wrapper.name] { 536 return nil 537 } 538 539 launched[wrapper.name] = true 540 history = append(history, wrapper.name) 541 542 for _, dep := range wrapper.service.DependentServices() { 543 target := wrappers[dep.Target] 544 if target == nil { 545 log.Errorf("Failed to find %s", dep.Target) 546 continue 547 } 548 549 if utils.Contains(history, dep.Target) { 550 cycle := strings.Join(append(history, dep.Target), "->") 551 if dep.Optional { 552 log.Debugf("Ignoring cycle for %s", cycle) 553 wrapper.IgnoreDep(dep.Target) 554 if cycleAction != nil { 555 var err error 556 log.Debugf("Running cycle action for %s", cycle) 557 err = cycleAction(target.service) 558 if err != nil { 559 return err 560 } 561 } 562 } else { 563 return fmt.Errorf("Cycle detected in path %s", cycle) 564 } 565 566 continue 567 } 568 569 err := p.startService(wrappers, history, selected, launched, target, action, cycleAction) 570 if err != nil { 571 return err 572 } 573 } 574 575 if isSelected(wrapper, selected) { 576 log.Debugf("Launching action for %s", wrapper.name) 577 go action(wrapper, wrappers) 578 } else { 579 wrapper.Ignore() 580 } 581 582 return nil 583 } 584 585 func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[string]*serviceWrapper, action wrapperAction, cycleAction serviceAction) error { 586 restart := false 587 wrapperList := []string{} 588 589 if start { 590 for _, name := range p.ServiceConfigs.Keys() { 591 wrapperList = append(wrapperList, name) 592 } 593 } else { 594 for _, wrapper := range wrappers { 595 if err := wrapper.Reset(); err != nil { 596 return err 597 } 598 } 599 wrapperList = p.reload 600 } 601 602 p.loadWrappers(wrappers, wrapperList) 603 p.reload = []string{} 604 605 // check service name 606 for s := range selected { 607 if wrappers[s] == nil { 608 return errors.New("No such service: " + s) 609 } 610 } 611 612 launched := map[string]bool{} 613 614 for _, wrapper := range wrappers { 615 if err := p.startService(wrappers, []string{}, selected, launched, wrapper, action, cycleAction); err != nil { 616 return err 617 } 618 } 619 620 var firstError error 621 622 for _, wrapper := range wrappers { 623 if !isSelected(wrapper, selected) { 624 continue 625 } 626 if err := wrapper.Wait(); err == ErrRestart { 627 restart = true 628 } else if err != nil { 629 log.Errorf("Failed to start: %s : %v", wrapper.name, err) 630 if firstError == nil { 631 firstError = err 632 } 633 } 634 } 635 636 if restart { 637 if p.ReloadCallback != nil { 638 if err := p.ReloadCallback(); err != nil { 639 log.Errorf("Failed calling callback: %v", err) 640 } 641 } 642 return p.traverse(false, selected, wrappers, action, cycleAction) 643 } 644 return firstError 645 } 646 647 // AddListener adds the specified listener to the project. 648 // This implements implicitly events.Emitter. 649 func (p *Project) AddListener(c chan<- events.Event) { 650 if !p.hasListeners { 651 for _, l := range p.listeners { 652 close(l) 653 } 654 p.hasListeners = true 655 p.listeners = []chan<- events.Event{c} 656 } else { 657 p.listeners = append(p.listeners, c) 658 } 659 } 660 661 // Notify notifies all project listener with the specified eventType, service name and datas. 662 // This implements implicitly events.Notifier interface. 663 func (p *Project) Notify(eventType events.EventType, serviceName string, data map[string]string) { 664 if eventType == events.NoEvent { 665 return 666 } 667 668 event := events.Event{ 669 EventType: eventType, 670 ServiceName: serviceName, 671 Data: data, 672 } 673 674 for _, l := range p.listeners { 675 l <- event 676 } 677 }