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