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  }