package project import ( "errors" "fmt" "os" "path" "path/filepath" "strings" "golang.org/x/net/context" "github.com/docker/libcompose/config" "github.com/docker/libcompose/logger" "github.com/docker/libcompose/lookup" "github.com/docker/libcompose/project/events" "github.com/docker/libcompose/utils" "github.com/docker/libcompose/yaml" log "github.com/sirupsen/logrus" ) // ComposeVersion is name of docker-compose.yml file syntax supported version const ComposeVersion = "1.5.0" type wrapperAction func(*serviceWrapper, map[string]*serviceWrapper) type serviceAction func(service Service) error // Project holds libcompose project information. type Project struct { Name string ServiceConfigs *config.ServiceConfigs VolumeConfigs map[string]*config.VolumeConfig NetworkConfigs map[string]*config.NetworkConfig Files []string ReloadCallback func() error ParseOptions *config.ParseOptions runtime RuntimeProject networks Networks volumes Volumes configVersion string context *Context reload []string upCount int listeners []chan<- events.Event hasListeners bool } // NewProject creates a new project with the specified context. func NewProject(context *Context, runtime RuntimeProject, parseOptions *config.ParseOptions) *Project { p := &Project{ context: context, runtime: runtime, ParseOptions: parseOptions, ServiceConfigs: config.NewServiceConfigs(), VolumeConfigs: make(map[string]*config.VolumeConfig), NetworkConfigs: make(map[string]*config.NetworkConfig), } if context.LoggerFactory == nil { context.LoggerFactory = &logger.NullLogger{} } if context.ResourceLookup == nil { context.ResourceLookup = &lookup.FileResourceLookup{} } if context.EnvironmentLookup == nil { var envPath, absPath, cwd string var err error if len(context.ComposeFiles) > 0 { absPath, err = filepath.Abs(context.ComposeFiles[0]) dir, _ := path.Split(absPath) envPath = filepath.Join(dir, ".env") } else { cwd, err = os.Getwd() envPath = filepath.Join(cwd, ".env") } if err != nil { log.Errorf("Could not get the rooted path name to the current directory: %v", err) return nil } context.EnvironmentLookup = &lookup.ComposableEnvLookup{ Lookups: []config.EnvironmentLookup{ &lookup.EnvfileLookup{ Path: envPath, }, &lookup.OsEnvLookup{}, }, } } context.Project = p p.listeners = []chan<- events.Event{NewDefaultListener(p)} return p } // Parse populates project information based on its context. It sets up the name, // the composefile and the composebytes (the composefile content). func (p *Project) Parse() error { err := p.context.open() if err != nil { return err } p.Name = p.context.ProjectName p.Files = p.context.ComposeFiles if len(p.Files) == 1 && p.Files[0] == "-" { p.Files = []string{"."} } if p.context.ComposeBytes != nil { for i, composeBytes := range p.context.ComposeBytes { file := "" if i < len(p.context.ComposeFiles) { file = p.Files[i] } if err := p.load(file, composeBytes); err != nil { return err } } } return nil } // CreateService creates a service with the specified name based. If there // is no config in the project for this service, it will return an error. func (p *Project) CreateService(name string) (Service, error) { existing, ok := p.GetServiceConfig(name) if !ok { return nil, fmt.Errorf("Failed to find service: %s", name) } // Copy because we are about to modify the environment config := *existing if p.context.EnvironmentLookup != nil { parsedEnv := make([]string, 0, len(config.Environment)) for _, env := range config.Environment { parts := strings.SplitN(env, "=", 2) if len(parts) > 1 { parsedEnv = append(parsedEnv, env) continue } else { env = parts[0] } for _, value := range p.context.EnvironmentLookup.Lookup(env, &config) { parsedEnv = append(parsedEnv, value) } } config.Environment = parsedEnv // check the environment for extra build Args that are set but not given a value in the compose file for arg, value := range config.Build.Args { if *value == "\x00" { envValue := p.context.EnvironmentLookup.Lookup(arg, &config) // depending on what we get back we do different things switch l := len(envValue); l { case 0: delete(config.Build.Args, arg) case 1: parts := strings.SplitN(envValue[0], "=", 2) config.Build.Args[parts[0]] = &parts[1] default: return nil, fmt.Errorf("tried to set Build Arg %#v to multi-value %#v", arg, envValue) } } } } return p.context.ServiceFactory.Create(p, name, &config) } // AddConfig adds the specified service config for the specified name. func (p *Project) AddConfig(name string, config *config.ServiceConfig) error { p.Notify(events.ServiceAdd, name, nil) p.ServiceConfigs.Add(name, config) p.reload = append(p.reload, name) return nil } // AddVolumeConfig adds the specified volume config for the specified name. func (p *Project) AddVolumeConfig(name string, config *config.VolumeConfig) error { p.Notify(events.VolumeAdd, name, nil) p.VolumeConfigs[name] = config return nil } // AddNetworkConfig adds the specified network config for the specified name. func (p *Project) AddNetworkConfig(name string, config *config.NetworkConfig) error { p.Notify(events.NetworkAdd, name, nil) p.NetworkConfigs[name] = config return nil } // Load loads the specified byte array (the composefile content) and adds the // service configuration to the project. // FIXME is it needed ? func (p *Project) Load(bytes []byte) error { return p.load("", bytes) } func (p *Project) load(file string, bytes []byte) error { version, serviceConfigs, volumeConfigs, networkConfigs, err := config.Merge(p.ServiceConfigs, p.context.EnvironmentLookup, p.context.ResourceLookup, file, bytes, p.ParseOptions) if err != nil { log.Errorf("Could not parse config for project %s : %v", p.Name, err) return err } p.configVersion = version for name, config := range volumeConfigs { err := p.AddVolumeConfig(name, config) if err != nil { return err } } for name, config := range networkConfigs { err := p.AddNetworkConfig(name, config) if err != nil { return err } } for name, config := range serviceConfigs { err := p.AddConfig(name, config) if err != nil { return err } } // Update network configuration a little bit p.handleNetworkConfig() p.handleVolumeConfig() if p.context.NetworksFactory != nil { networks, err := p.context.NetworksFactory.Create(p.Name, p.NetworkConfigs, p.ServiceConfigs, p.isNetworkEnabled()) if err != nil { return err } p.networks = networks } if p.context.VolumesFactory != nil { volumes, err := p.context.VolumesFactory.Create(p.Name, p.VolumeConfigs, p.ServiceConfigs, p.isVolumeEnabled()) if err != nil { return err } p.volumes = volumes } return nil } func (p *Project) handleNetworkConfig() { if p.isNetworkEnabled() { for _, serviceName := range p.ServiceConfigs.Keys() { serviceConfig, _ := p.ServiceConfigs.Get(serviceName) if serviceConfig.NetworkMode != "" { continue } if serviceConfig.Networks == nil || len(serviceConfig.Networks.Networks) == 0 { // Add default as network serviceConfig.Networks = &yaml.Networks{ Networks: []*yaml.Network{ { Name: "default", RealName: fmt.Sprintf("%s_%s", p.Name, "default"), }, }, } p.AddNetworkConfig("default", &config.NetworkConfig{}) } // Consolidate the name of the network // FIXME(vdemeester) probably shouldn't be there, maybe move that to interface/factory for _, network := range serviceConfig.Networks.Networks { net, ok := p.NetworkConfigs[network.Name] if ok && net != nil { if net.External.External { network.RealName = network.Name if net.External.Name != "" { network.RealName = net.External.Name } } else { network.RealName = p.Name + "_" + network.Name } } else { network.RealName = p.Name + "_" + network.Name p.NetworkConfigs[network.Name] = &config.NetworkConfig{ External: yaml.External{External: false}, } } // Ignoring if we don't find the network, it will be catched later } } } } func (p *Project) isNetworkEnabled() bool { return p.configVersion == "2" } func (p *Project) handleVolumeConfig() { if p.isVolumeEnabled() { for _, serviceName := range p.ServiceConfigs.Keys() { serviceConfig, _ := p.ServiceConfigs.Get(serviceName) // Consolidate the name of the volume // FIXME(vdemeester) probably shouldn't be there, maybe move that to interface/factory if serviceConfig.Volumes == nil { continue } for _, volume := range serviceConfig.Volumes.Volumes { if !IsNamedVolume(volume.Source) { continue } vol, ok := p.VolumeConfigs[volume.Source] if !ok || vol == nil { continue } if vol.External.External { if vol.External.Name != "" { volume.Source = vol.External.Name } } else { volume.Source = p.Name + "_" + volume.Source } } } } } func (p *Project) isVolumeEnabled() bool { return p.configVersion == "2" } // initialize sets up required element for project before any action (on project and service). // This means it's not needed to be called on Config for example. func (p *Project) initialize(ctx context.Context) error { if p.networks != nil { if err := p.networks.Initialize(ctx); err != nil { return err } } if p.volumes != nil { if err := p.volumes.Initialize(ctx); err != nil { return err } } return nil } func (p *Project) loadWrappers(wrappers map[string]*serviceWrapper, servicesToConstruct []string) error { for _, name := range servicesToConstruct { wrapper, err := newServiceWrapper(name, p) if err != nil { return err } wrappers[name] = wrapper } return nil } func (p *Project) perform(start, done events.EventType, services []string, action wrapperAction, cycleAction serviceAction) error { p.Notify(start, "", nil) err := p.forEach(services, action, cycleAction) p.Notify(done, "", nil) return err } func isSelected(wrapper *serviceWrapper, selected map[string]bool) bool { return len(selected) == 0 || selected[wrapper.name] } func (p *Project) forEach(services []string, action wrapperAction, cycleAction serviceAction) error { selected := make(map[string]bool) wrappers := make(map[string]*serviceWrapper) for _, s := range services { selected[s] = true } return p.traverse(true, selected, wrappers, action, cycleAction) } func (p *Project) startService(wrappers map[string]*serviceWrapper, history []string, selected, launched map[string]bool, wrapper *serviceWrapper, action wrapperAction, cycleAction serviceAction) error { if launched[wrapper.name] { return nil } launched[wrapper.name] = true history = append(history, wrapper.name) for _, dep := range wrapper.service.DependentServices() { target := wrappers[dep.Target] if target == nil { log.Debugf("Failed to find %s", dep.Target) return fmt.Errorf("Service '%s' has a link to service '%s' which is undefined", wrapper.name, dep.Target) } if utils.Contains(history, dep.Target) { cycle := strings.Join(append(history, dep.Target), "->") if dep.Optional { log.Debugf("Ignoring cycle for %s", cycle) wrapper.IgnoreDep(dep.Target) if cycleAction != nil { var err error log.Debugf("Running cycle action for %s", cycle) err = cycleAction(target.service) if err != nil { return err } } } else { return fmt.Errorf("Cycle detected in path %s", cycle) } continue } err := p.startService(wrappers, history, selected, launched, target, action, cycleAction) if err != nil { return err } } if isSelected(wrapper, selected) { log.Debugf("Launching action for %s", wrapper.name) go action(wrapper, wrappers) } else { wrapper.Ignore() } return nil } func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[string]*serviceWrapper, action wrapperAction, cycleAction serviceAction) error { restart := false wrapperList := []string{} if start { for _, name := range p.ServiceConfigs.Keys() { wrapperList = append(wrapperList, name) } } else { for _, wrapper := range wrappers { if err := wrapper.Reset(); err != nil { return err } } wrapperList = p.reload } p.loadWrappers(wrappers, wrapperList) p.reload = []string{} // check service name for s := range selected { if wrappers[s] == nil { return errors.New("No such service: " + s) } } launched := map[string]bool{} for _, wrapper := range wrappers { if err := p.startService(wrappers, []string{}, selected, launched, wrapper, action, cycleAction); err != nil { return err } } var firstError error for _, wrapper := range wrappers { if !isSelected(wrapper, selected) { continue } if err := wrapper.Wait(); err == ErrRestart { restart = true } else if err != nil { log.Errorf("Failed to start: %s : %v", wrapper.name, err) if firstError == nil { firstError = err } } } if restart { if p.ReloadCallback != nil { if err := p.ReloadCallback(); err != nil { log.Errorf("Failed calling callback: %v", err) } } return p.traverse(false, selected, wrappers, action, cycleAction) } return firstError } // AddListener adds the specified listener to the project. // This implements implicitly events.Emitter. func (p *Project) AddListener(c chan<- events.Event) { if !p.hasListeners { for _, l := range p.listeners { close(l) } p.hasListeners = true p.listeners = []chan<- events.Event{c} } else { p.listeners = append(p.listeners, c) } } // Notify notifies all project listener with the specified eventType, service name and datas. // This implements implicitly events.Notifier interface. func (p *Project) Notify(eventType events.EventType, serviceName string, data map[string]string) { if eventType == events.NoEvent { return } event := events.Event{ EventType: eventType, ServiceName: serviceName, Data: data, } for _, l := range p.listeners { l <- event } } // GetServiceConfig looks up a service config for a given service name, returning the ServiceConfig // object and a bool flag indicating whether it was found func (p *Project) GetServiceConfig(name string) (*config.ServiceConfig, bool) { return p.ServiceConfigs.Get(name) } // IsNamedVolume returns whether the specified volume (string) is a named volume or not. func IsNamedVolume(volume string) bool { return !strings.HasPrefix(volume, ".") && !strings.HasPrefix(volume, "/") && !strings.HasPrefix(volume, "~") }