github.com/swisscom/cloudfoundry-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  }