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 }