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  }