project.go 20 KB

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