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