github.com/docker/libcompose@v0.4.1-0.20210616120443-2a046c0bdbf2/project/project.go (about)

     1  package project
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"golang.org/x/net/context"
    12  
    13  	"github.com/docker/libcompose/config"
    14  	"github.com/docker/libcompose/logger"
    15  	"github.com/docker/libcompose/lookup"
    16  	"github.com/docker/libcompose/project/events"
    17  	"github.com/docker/libcompose/utils"
    18  	"github.com/docker/libcompose/yaml"
    19  	log "github.com/sirupsen/logrus"
    20  )
    21  
    22  // ComposeVersion is name of docker-compose.yml file syntax supported version
    23  const ComposeVersion = "1.5.0"
    24  
    25  type wrapperAction func(*serviceWrapper, map[string]*serviceWrapper)
    26  type serviceAction func(service Service) error
    27  
    28  // Project holds libcompose project information.
    29  type Project struct {
    30  	Name           string
    31  	ServiceConfigs *config.ServiceConfigs
    32  	VolumeConfigs  map[string]*config.VolumeConfig
    33  	NetworkConfigs map[string]*config.NetworkConfig
    34  	Files          []string
    35  	ReloadCallback func() error
    36  	ParseOptions   *config.ParseOptions
    37  
    38  	runtime       RuntimeProject
    39  	networks      Networks
    40  	volumes       Volumes
    41  	configVersion string
    42  	context       *Context
    43  	reload        []string
    44  	upCount       int
    45  	listeners     []chan<- events.Event
    46  	hasListeners  bool
    47  }
    48  
    49  // NewProject creates a new project with the specified context.
    50  func NewProject(context *Context, runtime RuntimeProject, parseOptions *config.ParseOptions) *Project {
    51  	p := &Project{
    52  		context:        context,
    53  		runtime:        runtime,
    54  		ParseOptions:   parseOptions,
    55  		ServiceConfigs: config.NewServiceConfigs(),
    56  		VolumeConfigs:  make(map[string]*config.VolumeConfig),
    57  		NetworkConfigs: make(map[string]*config.NetworkConfig),
    58  	}
    59  
    60  	if context.LoggerFactory == nil {
    61  		context.LoggerFactory = &logger.NullLogger{}
    62  	}
    63  
    64  	if context.ResourceLookup == nil {
    65  		context.ResourceLookup = &lookup.FileResourceLookup{}
    66  	}
    67  
    68  	if context.EnvironmentLookup == nil {
    69  		var envPath, absPath, cwd string
    70  		var err error
    71  		if len(context.ComposeFiles) > 0 {
    72  			absPath, err = filepath.Abs(context.ComposeFiles[0])
    73  			dir, _ := path.Split(absPath)
    74  			envPath = filepath.Join(dir, ".env")
    75  		} else {
    76  			cwd, err = os.Getwd()
    77  			envPath = filepath.Join(cwd, ".env")
    78  		}
    79  
    80  		if err != nil {
    81  			log.Errorf("Could not get the rooted path name to the current directory: %v", err)
    82  			return nil
    83  		}
    84  		context.EnvironmentLookup = &lookup.ComposableEnvLookup{
    85  			Lookups: []config.EnvironmentLookup{
    86  				&lookup.EnvfileLookup{
    87  					Path: envPath,
    88  				},
    89  				&lookup.OsEnvLookup{},
    90  			},
    91  		}
    92  	}
    93  
    94  	context.Project = p
    95  
    96  	p.listeners = []chan<- events.Event{NewDefaultListener(p)}
    97  
    98  	return p
    99  }
   100  
   101  // Parse populates project information based on its context. It sets up the name,
   102  // the composefile and the composebytes (the composefile content).
   103  func (p *Project) Parse() error {
   104  	err := p.context.open()
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	p.Name = p.context.ProjectName
   110  
   111  	p.Files = p.context.ComposeFiles
   112  
   113  	if len(p.Files) == 1 && p.Files[0] == "-" {
   114  		p.Files = []string{"."}
   115  	}
   116  
   117  	if p.context.ComposeBytes != nil {
   118  		for i, composeBytes := range p.context.ComposeBytes {
   119  			file := ""
   120  			if i < len(p.context.ComposeFiles) {
   121  				file = p.Files[i]
   122  			}
   123  			if err := p.load(file, composeBytes); err != nil {
   124  				return err
   125  			}
   126  		}
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  // CreateService creates a service with the specified name based. If there
   133  // is no config in the project for this service, it will return an error.
   134  func (p *Project) CreateService(name string) (Service, error) {
   135  	existing, ok := p.GetServiceConfig(name)
   136  	if !ok {
   137  		return nil, fmt.Errorf("Failed to find service: %s", name)
   138  	}
   139  
   140  	// Copy because we are about to modify the environment
   141  	config := *existing
   142  
   143  	if p.context.EnvironmentLookup != nil {
   144  		parsedEnv := make([]string, 0, len(config.Environment))
   145  
   146  		for _, env := range config.Environment {
   147  			parts := strings.SplitN(env, "=", 2)
   148  			if len(parts) > 1 {
   149  				parsedEnv = append(parsedEnv, env)
   150  				continue
   151  			} else {
   152  				env = parts[0]
   153  			}
   154  
   155  			for _, value := range p.context.EnvironmentLookup.Lookup(env, &config) {
   156  				parsedEnv = append(parsedEnv, value)
   157  			}
   158  		}
   159  
   160  		config.Environment = parsedEnv
   161  
   162  		// check the environment for extra build Args that are set but not given a value in the compose file
   163  		for arg, value := range config.Build.Args {
   164  			if *value == "\x00" {
   165  				envValue := p.context.EnvironmentLookup.Lookup(arg, &config)
   166  				// depending on what we get back we do different things
   167  				switch l := len(envValue); l {
   168  				case 0:
   169  					delete(config.Build.Args, arg)
   170  				case 1:
   171  					parts := strings.SplitN(envValue[0], "=", 2)
   172  					config.Build.Args[parts[0]] = &parts[1]
   173  				default:
   174  					return nil, fmt.Errorf("tried to set Build Arg %#v to multi-value %#v", arg, envValue)
   175  				}
   176  			}
   177  		}
   178  	}
   179  
   180  	return p.context.ServiceFactory.Create(p, name, &config)
   181  }
   182  
   183  // AddConfig adds the specified service config for the specified name.
   184  func (p *Project) AddConfig(name string, config *config.ServiceConfig) error {
   185  	p.Notify(events.ServiceAdd, name, nil)
   186  
   187  	p.ServiceConfigs.Add(name, config)
   188  	p.reload = append(p.reload, name)
   189  
   190  	return nil
   191  }
   192  
   193  // AddVolumeConfig adds the specified volume config for the specified name.
   194  func (p *Project) AddVolumeConfig(name string, config *config.VolumeConfig) error {
   195  	p.Notify(events.VolumeAdd, name, nil)
   196  	p.VolumeConfigs[name] = config
   197  	return nil
   198  }
   199  
   200  // AddNetworkConfig adds the specified network config for the specified name.
   201  func (p *Project) AddNetworkConfig(name string, config *config.NetworkConfig) error {
   202  	p.Notify(events.NetworkAdd, name, nil)
   203  	p.NetworkConfigs[name] = config
   204  	return nil
   205  }
   206  
   207  // Load loads the specified byte array (the composefile content) and adds the
   208  // service configuration to the project.
   209  // FIXME is it needed ?
   210  func (p *Project) Load(bytes []byte) error {
   211  	return p.load("", bytes)
   212  }
   213  
   214  func (p *Project) load(file string, bytes []byte) error {
   215  	version, serviceConfigs, volumeConfigs, networkConfigs, err := config.Merge(p.ServiceConfigs, p.context.EnvironmentLookup, p.context.ResourceLookup, file, bytes, p.ParseOptions)
   216  	if err != nil {
   217  		log.Errorf("Could not parse config for project %s : %v", p.Name, err)
   218  		return err
   219  	}
   220  
   221  	p.configVersion = version
   222  
   223  	for name, config := range volumeConfigs {
   224  		err := p.AddVolumeConfig(name, config)
   225  		if err != nil {
   226  			return err
   227  		}
   228  	}
   229  
   230  	for name, config := range networkConfigs {
   231  		err := p.AddNetworkConfig(name, config)
   232  		if err != nil {
   233  			return err
   234  		}
   235  	}
   236  
   237  	for name, config := range serviceConfigs {
   238  		err := p.AddConfig(name, config)
   239  		if err != nil {
   240  			return err
   241  		}
   242  	}
   243  
   244  	// Update network configuration a little bit
   245  	p.handleNetworkConfig()
   246  	p.handleVolumeConfig()
   247  
   248  	if p.context.NetworksFactory != nil {
   249  		networks, err := p.context.NetworksFactory.Create(p.Name, p.NetworkConfigs, p.ServiceConfigs, p.isNetworkEnabled())
   250  		if err != nil {
   251  			return err
   252  		}
   253  
   254  		p.networks = networks
   255  	}
   256  
   257  	if p.context.VolumesFactory != nil {
   258  		volumes, err := p.context.VolumesFactory.Create(p.Name, p.VolumeConfigs, p.ServiceConfigs, p.isVolumeEnabled())
   259  		if err != nil {
   260  			return err
   261  		}
   262  
   263  		p.volumes = volumes
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  func (p *Project) handleNetworkConfig() {
   270  	if p.isNetworkEnabled() {
   271  		for _, serviceName := range p.ServiceConfigs.Keys() {
   272  			serviceConfig, _ := p.ServiceConfigs.Get(serviceName)
   273  			if serviceConfig.NetworkMode != "" {
   274  				continue
   275  			}
   276  			if serviceConfig.Networks == nil || len(serviceConfig.Networks.Networks) == 0 {
   277  				// Add default as network
   278  				serviceConfig.Networks = &yaml.Networks{
   279  					Networks: []*yaml.Network{
   280  						{
   281  							Name:     "default",
   282  							RealName: fmt.Sprintf("%s_%s", p.Name, "default"),
   283  						},
   284  					},
   285  				}
   286  				p.AddNetworkConfig("default", &config.NetworkConfig{})
   287  			}
   288  			// Consolidate the name of the network
   289  			// FIXME(vdemeester) probably shouldn't be there, maybe move that to interface/factory
   290  			for _, network := range serviceConfig.Networks.Networks {
   291  				net, ok := p.NetworkConfigs[network.Name]
   292  				if ok && net != nil {
   293  					if net.External.External {
   294  						network.RealName = network.Name
   295  						if net.External.Name != "" {
   296  							network.RealName = net.External.Name
   297  						}
   298  					} else {
   299  						network.RealName = p.Name + "_" + network.Name
   300  					}
   301  				} else {
   302  					network.RealName = p.Name + "_" + network.Name
   303  
   304  					p.NetworkConfigs[network.Name] = &config.NetworkConfig{
   305  						External: yaml.External{External: false},
   306  					}
   307  				}
   308  				// Ignoring if we don't find the network, it will be catched later
   309  			}
   310  		}
   311  	}
   312  }
   313  
   314  func (p *Project) isNetworkEnabled() bool {
   315  	return p.configVersion == "2"
   316  }
   317  
   318  func (p *Project) handleVolumeConfig() {
   319  	if p.isVolumeEnabled() {
   320  		for _, serviceName := range p.ServiceConfigs.Keys() {
   321  			serviceConfig, _ := p.ServiceConfigs.Get(serviceName)
   322  			// Consolidate the name of the volume
   323  			// FIXME(vdemeester) probably shouldn't be there, maybe move that to interface/factory
   324  			if serviceConfig.Volumes == nil {
   325  				continue
   326  			}
   327  			for _, volume := range serviceConfig.Volumes.Volumes {
   328  				if !IsNamedVolume(volume.Source) {
   329  					continue
   330  				}
   331  
   332  				vol, ok := p.VolumeConfigs[volume.Source]
   333  				if !ok || vol == nil {
   334  					continue
   335  				}
   336  
   337  				if vol.External.External {
   338  					if vol.External.Name != "" {
   339  						volume.Source = vol.External.Name
   340  					}
   341  				} else {
   342  					volume.Source = p.Name + "_" + volume.Source
   343  				}
   344  			}
   345  		}
   346  	}
   347  }
   348  
   349  func (p *Project) isVolumeEnabled() bool {
   350  	return p.configVersion == "2"
   351  }
   352  
   353  // initialize sets up required element for project before any action (on project and service).
   354  // This means it's not needed to be called on Config for example.
   355  func (p *Project) initialize(ctx context.Context) error {
   356  	if p.networks != nil {
   357  		if err := p.networks.Initialize(ctx); err != nil {
   358  			return err
   359  		}
   360  	}
   361  	if p.volumes != nil {
   362  		if err := p.volumes.Initialize(ctx); err != nil {
   363  			return err
   364  		}
   365  	}
   366  	return nil
   367  }
   368  
   369  func (p *Project) loadWrappers(wrappers map[string]*serviceWrapper, servicesToConstruct []string) error {
   370  	for _, name := range servicesToConstruct {
   371  		wrapper, err := newServiceWrapper(name, p)
   372  		if err != nil {
   373  			return err
   374  		}
   375  		wrappers[name] = wrapper
   376  	}
   377  
   378  	return nil
   379  }
   380  
   381  func (p *Project) perform(start, done events.EventType, services []string, action wrapperAction, cycleAction serviceAction) error {
   382  	p.Notify(start, "", nil)
   383  
   384  	err := p.forEach(services, action, cycleAction)
   385  
   386  	p.Notify(done, "", nil)
   387  	return err
   388  }
   389  
   390  func isSelected(wrapper *serviceWrapper, selected map[string]bool) bool {
   391  	return len(selected) == 0 || selected[wrapper.name]
   392  }
   393  
   394  func (p *Project) forEach(services []string, action wrapperAction, cycleAction serviceAction) error {
   395  	selected := make(map[string]bool)
   396  	wrappers := make(map[string]*serviceWrapper)
   397  
   398  	for _, s := range services {
   399  		selected[s] = true
   400  	}
   401  
   402  	return p.traverse(true, selected, wrappers, action, cycleAction)
   403  }
   404  
   405  func (p *Project) startService(wrappers map[string]*serviceWrapper, history []string, selected, launched map[string]bool, wrapper *serviceWrapper, action wrapperAction, cycleAction serviceAction) error {
   406  	if launched[wrapper.name] {
   407  		return nil
   408  	}
   409  
   410  	launched[wrapper.name] = true
   411  	history = append(history, wrapper.name)
   412  
   413  	for _, dep := range wrapper.service.DependentServices() {
   414  		target := wrappers[dep.Target]
   415  		if target == nil {
   416  			log.Debugf("Failed to find %s", dep.Target)
   417  			return fmt.Errorf("Service '%s' has a link to service '%s' which is undefined", wrapper.name, dep.Target)
   418  		}
   419  
   420  		if utils.Contains(history, dep.Target) {
   421  			cycle := strings.Join(append(history, dep.Target), "->")
   422  			if dep.Optional {
   423  				log.Debugf("Ignoring cycle for %s", cycle)
   424  				wrapper.IgnoreDep(dep.Target)
   425  				if cycleAction != nil {
   426  					var err error
   427  					log.Debugf("Running cycle action for %s", cycle)
   428  					err = cycleAction(target.service)
   429  					if err != nil {
   430  						return err
   431  					}
   432  				}
   433  			} else {
   434  				return fmt.Errorf("Cycle detected in path %s", cycle)
   435  			}
   436  
   437  			continue
   438  		}
   439  
   440  		err := p.startService(wrappers, history, selected, launched, target, action, cycleAction)
   441  		if err != nil {
   442  			return err
   443  		}
   444  	}
   445  
   446  	if isSelected(wrapper, selected) {
   447  		log.Debugf("Launching action for %s", wrapper.name)
   448  		go action(wrapper, wrappers)
   449  	} else {
   450  		wrapper.Ignore()
   451  	}
   452  
   453  	return nil
   454  }
   455  
   456  func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[string]*serviceWrapper, action wrapperAction, cycleAction serviceAction) error {
   457  	restart := false
   458  	wrapperList := []string{}
   459  
   460  	if start {
   461  		for _, name := range p.ServiceConfigs.Keys() {
   462  			wrapperList = append(wrapperList, name)
   463  		}
   464  	} else {
   465  		for _, wrapper := range wrappers {
   466  			if err := wrapper.Reset(); err != nil {
   467  				return err
   468  			}
   469  		}
   470  		wrapperList = p.reload
   471  	}
   472  
   473  	p.loadWrappers(wrappers, wrapperList)
   474  	p.reload = []string{}
   475  
   476  	// check service name
   477  	for s := range selected {
   478  		if wrappers[s] == nil {
   479  			return errors.New("No such service: " + s)
   480  		}
   481  	}
   482  
   483  	launched := map[string]bool{}
   484  
   485  	for _, wrapper := range wrappers {
   486  		if err := p.startService(wrappers, []string{}, selected, launched, wrapper, action, cycleAction); err != nil {
   487  			return err
   488  		}
   489  	}
   490  
   491  	var firstError error
   492  
   493  	for _, wrapper := range wrappers {
   494  		if !isSelected(wrapper, selected) {
   495  			continue
   496  		}
   497  		if err := wrapper.Wait(); err == ErrRestart {
   498  			restart = true
   499  		} else if err != nil {
   500  			log.Errorf("Failed to start: %s : %v", wrapper.name, err)
   501  			if firstError == nil {
   502  				firstError = err
   503  			}
   504  		}
   505  	}
   506  
   507  	if restart {
   508  		if p.ReloadCallback != nil {
   509  			if err := p.ReloadCallback(); err != nil {
   510  				log.Errorf("Failed calling callback: %v", err)
   511  			}
   512  		}
   513  		return p.traverse(false, selected, wrappers, action, cycleAction)
   514  	}
   515  	return firstError
   516  }
   517  
   518  // AddListener adds the specified listener to the project.
   519  // This implements implicitly events.Emitter.
   520  func (p *Project) AddListener(c chan<- events.Event) {
   521  	if !p.hasListeners {
   522  		for _, l := range p.listeners {
   523  			close(l)
   524  		}
   525  		p.hasListeners = true
   526  		p.listeners = []chan<- events.Event{c}
   527  	} else {
   528  		p.listeners = append(p.listeners, c)
   529  	}
   530  }
   531  
   532  // Notify notifies all project listener with the specified eventType, service name and datas.
   533  // This implements implicitly events.Notifier interface.
   534  func (p *Project) Notify(eventType events.EventType, serviceName string, data map[string]string) {
   535  	if eventType == events.NoEvent {
   536  		return
   537  	}
   538  
   539  	event := events.Event{
   540  		EventType:   eventType,
   541  		ServiceName: serviceName,
   542  		Data:        data,
   543  	}
   544  
   545  	for _, l := range p.listeners {
   546  		l <- event
   547  	}
   548  }
   549  
   550  // GetServiceConfig looks up a service config for a given service name, returning the ServiceConfig
   551  // object and a bool flag indicating whether it was found
   552  func (p *Project) GetServiceConfig(name string) (*config.ServiceConfig, bool) {
   553  	return p.ServiceConfigs.Get(name)
   554  }
   555  
   556  // IsNamedVolume returns whether the specified volume (string) is a named volume or not.
   557  func IsNamedVolume(volume string) bool {
   558  	return !strings.HasPrefix(volume, ".") && !strings.HasPrefix(volume, "/") && !strings.HasPrefix(volume, "~")
   559  }