github.com/cloudfoundry-attic/cli-with-i18n@v6.32.1-0.20171002233121-7401370d3b85+incompatible/cf/commands/application/start.go (about) 1 package application 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "sort" 8 "strconv" 9 "strings" 10 "time" 11 12 "sync" 13 14 "sync/atomic" 15 16 "code.cloudfoundry.org/cli/cf" 17 "code.cloudfoundry.org/cli/cf/api/appinstances" 18 "code.cloudfoundry.org/cli/cf/api/applications" 19 "code.cloudfoundry.org/cli/cf/api/logs" 20 "code.cloudfoundry.org/cli/cf/commandregistry" 21 "code.cloudfoundry.org/cli/cf/configuration/coreconfig" 22 "code.cloudfoundry.org/cli/cf/flags" 23 . "code.cloudfoundry.org/cli/cf/i18n" 24 "code.cloudfoundry.org/cli/cf/models" 25 "code.cloudfoundry.org/cli/cf/requirements" 26 "code.cloudfoundry.org/cli/cf/terminal" 27 ) 28 29 const ( 30 DefaultStagingTimeout = 15 * time.Minute 31 DefaultStartupTimeout = 5 * time.Minute 32 DefaultPingerThrottle = 5 * time.Second 33 ) 34 35 const LogMessageTypeStaging = "STG" 36 37 //go:generate counterfeiter . StagingWatcher 38 39 type StagingWatcher interface { 40 WatchStaging(app models.Application, orgName string, spaceName string, startCommand func(app models.Application) (models.Application, error)) (updatedApp models.Application, err error) 41 } 42 43 //go:generate counterfeiter . Starter 44 45 type Starter interface { 46 commandregistry.Command 47 SetStartTimeoutInSeconds(timeout int) 48 ApplicationStart(app models.Application, orgName string, spaceName string) (updatedApp models.Application, err error) 49 } 50 51 type Start struct { 52 ui terminal.UI 53 config coreconfig.Reader 54 appDisplayer Displayer 55 appReq requirements.ApplicationRequirement 56 appRepo applications.Repository 57 logRepo logs.Repository 58 appInstancesRepo appinstances.Repository 59 60 LogServerConnectionTimeout time.Duration 61 StartupTimeout time.Duration 62 StagingTimeout time.Duration 63 PingerThrottle time.Duration 64 } 65 66 func init() { 67 commandregistry.Register(&Start{}) 68 } 69 70 func (cmd *Start) MetaData() commandregistry.CommandMetadata { 71 return commandregistry.CommandMetadata{ 72 Name: "start", 73 ShortName: "st", 74 Description: T("Start an app"), 75 Usage: []string{ 76 T("CF_NAME start APP_NAME"), 77 }, 78 } 79 } 80 81 func (cmd *Start) Requirements(requirementsFactory requirements.Factory, fc flags.FlagContext) ([]requirements.Requirement, error) { 82 if len(fc.Args()) != 1 { 83 cmd.ui.Failed(T("Incorrect Usage. Requires an argument\n\n") + commandregistry.Commands.CommandUsage("start")) 84 return nil, fmt.Errorf("Incorrect usage: %d arguments of %d required", len(fc.Args()), 1) 85 } 86 87 cmd.appReq = requirementsFactory.NewApplicationRequirement(fc.Args()[0]) 88 89 reqs := []requirements.Requirement{ 90 requirementsFactory.NewLoginRequirement(), 91 requirementsFactory.NewTargetedSpaceRequirement(), 92 cmd.appReq, 93 } 94 95 return reqs, nil 96 } 97 98 func (cmd *Start) SetDependency(deps commandregistry.Dependency, pluginCall bool) commandregistry.Command { 99 cmd.ui = deps.UI 100 cmd.config = deps.Config 101 cmd.appRepo = deps.RepoLocator.GetApplicationRepository() 102 cmd.appInstancesRepo = deps.RepoLocator.GetAppInstancesRepository() 103 cmd.logRepo = deps.RepoLocator.GetLogsRepository() 104 cmd.LogServerConnectionTimeout = 20 * time.Second 105 cmd.PingerThrottle = DefaultPingerThrottle 106 107 if os.Getenv("CF_STAGING_TIMEOUT") != "" { 108 duration, err := strconv.ParseInt(os.Getenv("CF_STAGING_TIMEOUT"), 10, 64) 109 if err != nil { 110 cmd.ui.Failed(T("invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}", 111 map[string]interface{}{"Err": err})) 112 } 113 cmd.StagingTimeout = time.Duration(duration) * time.Minute 114 } else { 115 cmd.StagingTimeout = DefaultStagingTimeout 116 } 117 118 if os.Getenv("CF_STARTUP_TIMEOUT") != "" { 119 duration, err := strconv.ParseInt(os.Getenv("CF_STARTUP_TIMEOUT"), 10, 64) 120 if err != nil { 121 cmd.ui.Failed(T("invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}", 122 map[string]interface{}{"Err": err})) 123 } 124 cmd.StartupTimeout = time.Duration(duration) * time.Minute 125 } else { 126 cmd.StartupTimeout = DefaultStartupTimeout 127 } 128 129 appCommand := commandregistry.Commands.FindCommand("app") 130 appCommand = appCommand.SetDependency(deps, false) 131 cmd.appDisplayer = appCommand.(Displayer) 132 133 return cmd 134 } 135 136 func (cmd *Start) Execute(c flags.FlagContext) error { 137 _, err := cmd.ApplicationStart(cmd.appReq.GetApplication(), cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) 138 return err 139 } 140 141 func (cmd *Start) ApplicationStart(app models.Application, orgName, spaceName string) (models.Application, error) { 142 if app.State == "started" { 143 cmd.ui.Say(terminal.WarningColor(T("App ") + app.Name + T(" is already started"))) 144 return models.Application{}, nil 145 } 146 147 return cmd.WatchStaging(app, orgName, spaceName, func(app models.Application) (models.Application, error) { 148 cmd.ui.Say(T("Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", 149 map[string]interface{}{ 150 "AppName": terminal.EntityNameColor(app.Name), 151 "OrgName": terminal.EntityNameColor(orgName), 152 "SpaceName": terminal.EntityNameColor(spaceName), 153 "CurrentUser": terminal.EntityNameColor(cmd.config.Username())})) 154 155 state := "started" 156 return cmd.appRepo.Update(app.GUID, models.AppParams{State: &state}) 157 }) 158 } 159 160 func (cmd *Start) WatchStaging(app models.Application, orgName, spaceName string, start func(app models.Application) (models.Application, error)) (models.Application, error) { 161 stopChan := make(chan bool, 1) 162 163 loggingStartedWait := new(sync.WaitGroup) 164 loggingStartedWait.Add(1) 165 166 loggingDoneWait := new(sync.WaitGroup) 167 loggingDoneWait.Add(1) 168 169 go cmd.TailStagingLogs(app, stopChan, loggingStartedWait, loggingDoneWait) 170 171 loggingStartedWait.Wait() 172 173 updatedApp, err := start(app) 174 if err != nil { 175 return models.Application{}, err 176 } 177 178 isStaged, err := cmd.waitForInstancesToStage(updatedApp) 179 if err != nil { 180 return models.Application{}, err 181 } 182 183 stopChan <- true 184 185 loggingDoneWait.Wait() 186 187 cmd.ui.Say("") 188 189 if !isStaged { 190 return models.Application{}, fmt.Errorf("%s failed to stage within %f minutes", app.Name, cmd.StagingTimeout.Minutes()) 191 } 192 193 if app.InstanceCount > 0 { 194 err = cmd.waitForOneRunningInstance(updatedApp) 195 if err != nil { 196 return models.Application{}, err 197 } 198 cmd.ui.Say(terminal.HeaderColor(T("\nApp started\n"))) 199 cmd.ui.Say("") 200 } else { 201 cmd.ui.Say(terminal.HeaderColor(T("\nApp state changed to started, but note that it has 0 instances.\n"))) 202 cmd.ui.Say("") 203 } 204 cmd.ui.Ok() 205 206 //detectedstartcommand on first push is not present until starting completes 207 startedApp, err := cmd.appRepo.GetApp(updatedApp.GUID) 208 if err != nil { 209 return models.Application{}, err 210 } 211 212 var appStartCommand string 213 if app.Command == "" { 214 appStartCommand = startedApp.DetectedStartCommand 215 } else { 216 appStartCommand = startedApp.Command 217 } 218 219 cmd.ui.Say(T("\nApp {{.AppName}} was started using this command `{{.Command}}`\n", 220 map[string]interface{}{ 221 "AppName": terminal.EntityNameColor(startedApp.Name), 222 "Command": appStartCommand, 223 })) 224 225 err = cmd.appDisplayer.ShowApp(startedApp, orgName, spaceName) 226 if err != nil { 227 return models.Application{}, err 228 } 229 230 return updatedApp, nil 231 } 232 233 func (cmd *Start) SetStartTimeoutInSeconds(timeout int) { 234 cmd.StartupTimeout = time.Duration(timeout) * time.Second 235 } 236 237 type ConnectionType int 238 239 const ( 240 NoConnection ConnectionType = iota 241 ConnectionWasEstablished 242 ConnectionWasClosed 243 StoppedTrying 244 ) 245 246 func (cmd *Start) TailStagingLogs(app models.Application, stopChan chan bool, startWait, doneWait *sync.WaitGroup) { 247 var connectionStatus atomic.Value 248 connectionStatus.Store(NoConnection) 249 250 var once sync.Once 251 startWaitDone := func() { 252 startWait.Done() 253 } 254 255 onConnect := func() { 256 if connectionStatus.Load() != StoppedTrying { 257 connectionStatus.Store(ConnectionWasEstablished) 258 once.Do(startWaitDone) 259 } 260 } 261 262 timer := time.NewTimer(cmd.LogServerConnectionTimeout) 263 264 c := make(chan logs.Loggable) 265 e := make(chan error) 266 267 defer doneWait.Done() 268 269 go cmd.logRepo.TailLogsFor(app.GUID, onConnect, c, e) 270 271 for { 272 select { 273 case <-timer.C: 274 if connectionStatus.Load() == NoConnection { 275 connectionStatus.Store(StoppedTrying) 276 cmd.ui.Warn("timeout connecting to log server, no log will be shown") 277 once.Do(startWaitDone) 278 return 279 } 280 case msg, ok := <-c: 281 if !ok { 282 return 283 } else if msg.GetSourceName() == LogMessageTypeStaging { 284 cmd.ui.Say(msg.ToSimpleLog()) 285 } 286 287 case err, ok := <-e: 288 if ok { 289 if connectionStatus.Load() != ConnectionWasClosed { 290 cmd.ui.Warn(T("Warning: error tailing logs")) 291 cmd.ui.Say("%s", err) 292 once.Do(startWaitDone) 293 return 294 } 295 } 296 297 case <-stopChan: 298 if connectionStatus.Load() == ConnectionWasEstablished { 299 connectionStatus.Store(ConnectionWasClosed) 300 cmd.logRepo.Close() 301 } else { 302 return 303 } 304 } 305 } 306 } 307 308 func (cmd *Start) waitForInstancesToStage(app models.Application) (bool, error) { 309 stagingStartTime := time.Now() 310 311 var err error 312 313 if cmd.StagingTimeout == 0 { 314 app, err = cmd.appRepo.GetApp(app.GUID) 315 } else { 316 for app.PackageState != "STAGED" && app.PackageState != "FAILED" && time.Since(stagingStartTime) < cmd.StagingTimeout { 317 app, err = cmd.appRepo.GetApp(app.GUID) 318 if err != nil { 319 break 320 } 321 322 time.Sleep(cmd.PingerThrottle) 323 } 324 } 325 326 if err != nil { 327 return false, err 328 } 329 330 if app.PackageState == "FAILED" { 331 cmd.ui.Say("") 332 if app.StagingFailedReason == "NoAppDetectedError" { 333 return false, errors.New(T(`{{.Err}} 334 335 TIP: Buildpacks are detected when the "{{.PushCommand}}" is executed from within the directory that contains the app source code. 336 337 Use '{{.BuildpackCommand}}' to see a list of supported buildpacks. 338 339 Use '{{.Command}}' for more in depth log information.`, 340 map[string]interface{}{ 341 "Err": app.StagingFailedReason, 342 "PushCommand": terminal.CommandColor(fmt.Sprintf("%s push", cf.Name)), 343 "BuildpackCommand": terminal.CommandColor(fmt.Sprintf("%s buildpacks", cf.Name)), 344 "Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name, app.Name))})) 345 } 346 return false, errors.New(T("{{.Err}}\n\nTIP: use '{{.Command}}' for more information", 347 map[string]interface{}{ 348 "Err": app.StagingFailedReason, 349 "Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name, app.Name))})) 350 } 351 352 if time.Since(stagingStartTime) >= cmd.StagingTimeout { 353 return false, nil 354 } 355 356 return true, nil 357 } 358 359 func (cmd *Start) waitForOneRunningInstance(app models.Application) error { 360 timer := time.NewTimer(cmd.StartupTimeout) 361 362 for { 363 select { 364 case <-timer.C: 365 tipMsg := T("Start app timeout\n\nTIP: Application must be listening on the right port. Instead of hard coding the port, use the $PORT environment variable.") + "\n\n" 366 tipMsg += T("Use '{{.Command}}' for more information", map[string]interface{}{"Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name, app.Name))}) 367 368 return errors.New(tipMsg) 369 370 default: 371 count, err := cmd.fetchInstanceCount(app.GUID) 372 if err != nil { 373 cmd.ui.Warn("Could not fetch instance count: %s", err.Error()) 374 time.Sleep(cmd.PingerThrottle) 375 continue 376 } 377 378 cmd.ui.Say(instancesDetails(count)) 379 380 if count.running > 0 { 381 return nil 382 } 383 384 if count.flapping > 0 || count.crashed > 0 { 385 return fmt.Errorf(T("Start unsuccessful\n\nTIP: use '{{.Command}}' for more information", 386 map[string]interface{}{"Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name, app.Name))})) 387 } 388 389 time.Sleep(cmd.PingerThrottle) 390 } 391 } 392 } 393 394 type instanceCount struct { 395 running int 396 starting int 397 startingDetails map[string]struct{} 398 flapping int 399 down int 400 crashed int 401 total int 402 } 403 404 func (cmd Start) fetchInstanceCount(appGUID string) (instanceCount, error) { 405 count := instanceCount{ 406 startingDetails: make(map[string]struct{}), 407 } 408 409 instances, apiErr := cmd.appInstancesRepo.GetInstances(appGUID) 410 if apiErr != nil { 411 return instanceCount{}, apiErr 412 } 413 414 count.total = len(instances) 415 416 for _, inst := range instances { 417 switch inst.State { 418 case models.InstanceRunning: 419 count.running++ 420 case models.InstanceStarting: 421 count.starting++ 422 if inst.Details != "" { 423 count.startingDetails[inst.Details] = struct{}{} 424 } 425 case models.InstanceFlapping: 426 count.flapping++ 427 case models.InstanceDown: 428 count.down++ 429 case models.InstanceCrashed: 430 count.crashed++ 431 } 432 } 433 434 return count, nil 435 } 436 437 func instancesDetails(count instanceCount) string { 438 details := []string{fmt.Sprintf(T("{{.RunningCount}} of {{.TotalCount}} instances running", 439 map[string]interface{}{"RunningCount": count.running, "TotalCount": count.total}))} 440 441 if count.starting > 0 { 442 if len(count.startingDetails) == 0 { 443 details = append(details, fmt.Sprintf(T("{{.StartingCount}} starting", 444 map[string]interface{}{"StartingCount": count.starting}))) 445 } else { 446 info := []string{} 447 for d := range count.startingDetails { 448 info = append(info, d) 449 } 450 sort.Strings(info) 451 details = append(details, fmt.Sprintf(T("{{.StartingCount}} starting ({{.Details}})", 452 map[string]interface{}{ 453 "StartingCount": count.starting, 454 "Details": strings.Join(info, ", "), 455 }))) 456 } 457 } 458 459 if count.down > 0 { 460 details = append(details, fmt.Sprintf(T("{{.DownCount}} down", 461 map[string]interface{}{"DownCount": count.down}))) 462 } 463 464 if count.flapping > 0 { 465 details = append(details, fmt.Sprintf(T("{{.FlappingCount}} failing", 466 map[string]interface{}{"FlappingCount": count.flapping}))) 467 } 468 469 if count.crashed > 0 { 470 details = append(details, fmt.Sprintf(T("{{.CrashedCount}} crashed", 471 map[string]interface{}{"CrashedCount": count.crashed}))) 472 } 473 474 return strings.Join(details, ", ") 475 }