github.com/asifdxtreme/cli@v6.1.3-0.20150123051144-9ead8700b4ae+incompatible/cf/commands/application/start.go (about)

     1  package application
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	. "github.com/cloudfoundry/cli/cf/i18n"
    12  
    13  	"github.com/cloudfoundry/cli/cf"
    14  	"github.com/cloudfoundry/cli/cf/api"
    15  	"github.com/cloudfoundry/cli/cf/api/app_instances"
    16  	"github.com/cloudfoundry/cli/cf/api/applications"
    17  	"github.com/cloudfoundry/cli/cf/command_metadata"
    18  	"github.com/cloudfoundry/cli/cf/configuration/core_config"
    19  	"github.com/cloudfoundry/cli/cf/errors"
    20  	"github.com/cloudfoundry/cli/cf/models"
    21  	"github.com/cloudfoundry/cli/cf/requirements"
    22  	"github.com/cloudfoundry/cli/cf/terminal"
    23  	"github.com/cloudfoundry/loggregatorlib/logmessage"
    24  	"github.com/codegangsta/cli"
    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 Start struct {
    36  	ui               terminal.UI
    37  	config           core_config.Reader
    38  	appDisplayer     ApplicationDisplayer
    39  	appReq           requirements.ApplicationRequirement
    40  	appRepo          applications.ApplicationRepository
    41  	appInstancesRepo app_instances.AppInstancesRepository
    42  	logRepo          api.LogsRepository
    43  
    44  	StartupTimeout time.Duration
    45  	StagingTimeout time.Duration
    46  	PingerThrottle time.Duration
    47  }
    48  
    49  type ApplicationStarter interface {
    50  	SetStartTimeoutInSeconds(timeout int)
    51  	ApplicationStart(app models.Application, orgName string, spaceName string) (updatedApp models.Application, err error)
    52  }
    53  
    54  type ApplicationStagingWatcher interface {
    55  	ApplicationWatchStaging(app models.Application, orgName string, spaceName string, startCommand func(app models.Application) (models.Application, error)) (updatedApp models.Application, err error)
    56  }
    57  
    58  func NewStart(ui terminal.UI, config core_config.Reader, appDisplayer ApplicationDisplayer, appRepo applications.ApplicationRepository, appInstancesRepo app_instances.AppInstancesRepository, logRepo api.LogsRepository) (cmd *Start) {
    59  	cmd = new(Start)
    60  	cmd.ui = ui
    61  	cmd.config = config
    62  	cmd.appDisplayer = appDisplayer
    63  	cmd.appRepo = appRepo
    64  	cmd.appInstancesRepo = appInstancesRepo
    65  	cmd.logRepo = logRepo
    66  
    67  	cmd.PingerThrottle = DefaultPingerThrottle
    68  
    69  	if os.Getenv("CF_STAGING_TIMEOUT") != "" {
    70  		duration, err := strconv.ParseInt(os.Getenv("CF_STAGING_TIMEOUT"), 10, 64)
    71  		if err != nil {
    72  			cmd.ui.Failed(T("invalid value for env var CF_STAGING_TIMEOUT\n{{.Err}}",
    73  				map[string]interface{}{"Err": err}))
    74  		}
    75  		cmd.StagingTimeout = time.Duration(duration) * time.Minute
    76  	} else {
    77  		cmd.StagingTimeout = DefaultStagingTimeout
    78  	}
    79  
    80  	if os.Getenv("CF_STARTUP_TIMEOUT") != "" {
    81  		duration, err := strconv.ParseInt(os.Getenv("CF_STARTUP_TIMEOUT"), 10, 64)
    82  		if err != nil {
    83  			cmd.ui.Failed(T("invalid value for env var CF_STARTUP_TIMEOUT\n{{.Err}}",
    84  				map[string]interface{}{"Err": err}))
    85  		}
    86  		cmd.StartupTimeout = time.Duration(duration) * time.Minute
    87  	} else {
    88  		cmd.StartupTimeout = DefaultStartupTimeout
    89  	}
    90  
    91  	return
    92  }
    93  
    94  func (cmd *Start) Metadata() command_metadata.CommandMetadata {
    95  	return command_metadata.CommandMetadata{
    96  		Name:        "start",
    97  		ShortName:   "st",
    98  		Description: T("Start an app"),
    99  		Usage:       T("CF_NAME start APP_NAME"),
   100  	}
   101  }
   102  
   103  func (cmd *Start) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) {
   104  	if len(c.Args()) != 1 {
   105  		cmd.ui.FailWithUsage(c)
   106  	}
   107  
   108  	cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0])
   109  
   110  	reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement(), cmd.appReq}
   111  	return
   112  }
   113  
   114  func (cmd *Start) Run(c *cli.Context) {
   115  	cmd.ApplicationStart(cmd.appReq.GetApplication(), cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name)
   116  }
   117  
   118  func (cmd *Start) ApplicationStart(app models.Application, orgName, spaceName string) (updatedApp models.Application, err error) {
   119  	if app.State == "started" {
   120  		cmd.ui.Say(terminal.WarningColor(T("App ") + app.Name + T(" is already started")))
   121  		return
   122  	}
   123  
   124  	return cmd.ApplicationWatchStaging(app, orgName, spaceName, func(app models.Application) (models.Application, error) {
   125  		cmd.ui.Say(T("Starting app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...",
   126  			map[string]interface{}{
   127  				"AppName":     terminal.EntityNameColor(app.Name),
   128  				"OrgName":     terminal.EntityNameColor(orgName),
   129  				"SpaceName":   terminal.EntityNameColor(spaceName),
   130  				"CurrentUser": terminal.EntityNameColor(cmd.config.Username())}))
   131  
   132  		state := "STARTED"
   133  		return cmd.appRepo.Update(app.Guid, models.AppParams{State: &state})
   134  	})
   135  }
   136  
   137  func (cmd *Start) ApplicationWatchStaging(app models.Application, orgName, spaceName string, start func(app models.Application) (models.Application, error)) (updatedApp models.Application, err error) {
   138  	stopLoggingChan := make(chan bool, 1)
   139  	loggingStartedChan := make(chan bool)
   140  	doneLoggingChan := make(chan bool)
   141  
   142  	go cmd.tailStagingLogs(app, loggingStartedChan, doneLoggingChan)
   143  	go func() {
   144  		<-stopLoggingChan
   145  		cmd.logRepo.Close()
   146  	}()
   147  	<-loggingStartedChan // block until we have established connection to Loggregator
   148  
   149  	updatedApp, apiErr := start(app)
   150  	if apiErr != nil {
   151  		cmd.ui.Failed(apiErr.Error())
   152  		return
   153  	}
   154  
   155  	isStaged := cmd.waitForInstancesToStage(updatedApp)
   156  	stopLoggingChan <- true
   157  	<-doneLoggingChan
   158  
   159  	cmd.ui.Say("")
   160  
   161  	if !isStaged {
   162  		cmd.ui.Failed(fmt.Sprintf("%s failed to stage within %f minutes", app.Name, cmd.StagingTimeout.Minutes()))
   163  	}
   164  
   165  	cmd.waitForOneRunningInstance(updatedApp)
   166  	cmd.ui.Say(terminal.HeaderColor(T("\nApp started\n")))
   167  	cmd.ui.Say("")
   168  	cmd.ui.Ok()
   169  
   170  	//detectedstartcommand on first push is not present until starting completes
   171  	startedApp, apiErr := cmd.appRepo.Read(updatedApp.Name)
   172  	if err != nil {
   173  		cmd.ui.Failed(apiErr.Error())
   174  		return
   175  	}
   176  
   177  	var appStartCommand string
   178  	if app.Command == "" {
   179  		appStartCommand = startedApp.DetectedStartCommand
   180  	} else {
   181  		appStartCommand = startedApp.Command
   182  	}
   183  
   184  	cmd.ui.Say(T("\nApp {{.AppName}} was started using this command `{{.Command}}`\n",
   185  		map[string]interface{}{
   186  			"AppName": terminal.EntityNameColor(startedApp.Name),
   187  			"Command": appStartCommand,
   188  		}))
   189  
   190  	cmd.appDisplayer.ShowApp(startedApp, orgName, spaceName)
   191  	return
   192  }
   193  
   194  func (cmd *Start) SetStartTimeoutInSeconds(timeout int) {
   195  	cmd.StartupTimeout = time.Duration(timeout) * time.Second
   196  }
   197  
   198  func simpleLogMessageOutput(logMsg *logmessage.LogMessage) (msgText string) {
   199  	msgText = string(logMsg.GetMessage())
   200  	reg, err := regexp.Compile("[\n\r]+$")
   201  	if err != nil {
   202  		return
   203  	}
   204  	msgText = reg.ReplaceAllString(msgText, "")
   205  	return
   206  }
   207  
   208  func (cmd Start) tailStagingLogs(app models.Application, startChan, doneChan chan bool) {
   209  	onConnect := func() {
   210  		startChan <- true
   211  	}
   212  
   213  	err := cmd.logRepo.TailLogsFor(app.Guid, onConnect, func(msg *logmessage.LogMessage) {
   214  		if msg.GetSourceName() == LogMessageTypeStaging {
   215  			cmd.ui.Say(simpleLogMessageOutput(msg))
   216  		}
   217  	})
   218  
   219  	if err != nil {
   220  		cmd.ui.Warn(T("Warning: error tailing logs"))
   221  		cmd.ui.Say("%s", err)
   222  		startChan <- true
   223  	}
   224  
   225  	close(doneChan)
   226  }
   227  
   228  func isStagingError(err error) bool {
   229  	httpError, ok := err.(errors.HttpError)
   230  	return ok && httpError.ErrorCode() == errors.APP_NOT_STAGED
   231  }
   232  
   233  func (cmd Start) waitForInstancesToStage(app models.Application) bool {
   234  	stagingStartTime := time.Now()
   235  	_, err := cmd.appInstancesRepo.GetInstances(app.Guid)
   236  
   237  	for isStagingError(err) && time.Since(stagingStartTime) < cmd.StagingTimeout {
   238  		cmd.ui.Wait(cmd.PingerThrottle)
   239  		_, err = cmd.appInstancesRepo.GetInstances(app.Guid)
   240  	}
   241  
   242  	if err != nil && !isStagingError(err) {
   243  		cmd.ui.Say("")
   244  		cmd.ui.Failed(T("{{.Err}}\n\nTIP: use '{{.Command}}' for more information",
   245  			map[string]interface{}{
   246  				"Err":     err.Error(),
   247  				"Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))}))
   248  	}
   249  
   250  	if time.Since(stagingStartTime) >= cmd.StagingTimeout {
   251  		return false
   252  	}
   253  
   254  	return true
   255  }
   256  
   257  func (cmd Start) waitForOneRunningInstance(app models.Application) {
   258  	startupStartTime := time.Now()
   259  
   260  	for {
   261  		if time.Since(startupStartTime) > cmd.StartupTimeout {
   262  			cmd.ui.Failed(fmt.Sprintf(T("Start app timeout\n\nTIP: use '{{.Command}}' for more information",
   263  				map[string]interface{}{
   264  					"Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))})))
   265  			return
   266  		}
   267  
   268  		count, err := cmd.fetchInstanceCount(app.Guid)
   269  		if err != nil {
   270  			cmd.ui.Wait(cmd.PingerThrottle)
   271  			continue
   272  		}
   273  
   274  		cmd.ui.Say(instancesDetails(count))
   275  
   276  		if count.running > 0 {
   277  			return
   278  		}
   279  
   280  		if count.flapping > 0 {
   281  			cmd.ui.Failed(fmt.Sprintf(T("Start unsuccessful\n\nTIP: use '{{.Command}}' for more information",
   282  				map[string]interface{}{"Command": terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))})))
   283  			return
   284  		}
   285  
   286  		cmd.ui.Wait(cmd.PingerThrottle)
   287  	}
   288  }
   289  
   290  type instanceCount struct {
   291  	running  int
   292  	starting int
   293  	flapping int
   294  	down     int
   295  	total    int
   296  }
   297  
   298  func (cmd Start) fetchInstanceCount(appGuid string) (instanceCount, error) {
   299  	count := instanceCount{}
   300  
   301  	instances, apiErr := cmd.appInstancesRepo.GetInstances(appGuid)
   302  	if apiErr != nil {
   303  		return instanceCount{}, apiErr
   304  	}
   305  
   306  	count.total = len(instances)
   307  
   308  	for _, inst := range instances {
   309  		switch inst.State {
   310  		case models.InstanceRunning:
   311  			count.running++
   312  		case models.InstanceStarting:
   313  			count.starting++
   314  		case models.InstanceFlapping:
   315  			count.flapping++
   316  		case models.InstanceDown:
   317  			count.down++
   318  		}
   319  	}
   320  
   321  	return count, nil
   322  }
   323  
   324  func instancesDetails(count instanceCount) string {
   325  	details := []string{fmt.Sprintf(T("{{.RunningCount}} of {{.TotalCount}} instances running",
   326  		map[string]interface{}{"RunningCount": count.running, "TotalCount": count.total}))}
   327  
   328  	if count.starting > 0 {
   329  		details = append(details, fmt.Sprintf(T("{{.StartingCount}} starting",
   330  			map[string]interface{}{"StartingCount": count.starting})))
   331  	}
   332  
   333  	if count.down > 0 {
   334  		details = append(details, fmt.Sprintf(T("{{.DownCount}} down",
   335  			map[string]interface{}{"DownCount": count.down})))
   336  	}
   337  
   338  	if count.flapping > 0 {
   339  		details = append(details, fmt.Sprintf(T("{{.FlappingCount}} failing",
   340  			map[string]interface{}{"FlappingCount": count.flapping})))
   341  	}
   342  
   343  	return strings.Join(details, ", ")
   344  }