github.com/cloudfoundry-attic/ltc@v0.0.0-20151123212628-098adc7919fc/droplet_runner/command_factory/droplet_runner_command_factory.go (about)

     1  package command_factory
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/url"
     9  	"os"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"text/tabwriter"
    14  	"time"
    15  
    16  	"github.com/cloudfoundry-incubator/ltc/app_runner"
    17  	config_package "github.com/cloudfoundry-incubator/ltc/config"
    18  	"github.com/cloudfoundry-incubator/ltc/droplet_runner"
    19  	"github.com/cloudfoundry-incubator/ltc/droplet_runner/command_factory/cf_ignore"
    20  	"github.com/cloudfoundry-incubator/ltc/droplet_runner/command_factory/zipper"
    21  	"github.com/cloudfoundry-incubator/ltc/exit_handler/exit_codes"
    22  	"github.com/cloudfoundry-incubator/ltc/task_examiner"
    23  	"github.com/cloudfoundry-incubator/ltc/terminal/colors"
    24  	"github.com/codegangsta/cli"
    25  	"github.com/pivotal-golang/bytefmt"
    26  
    27  	app_runner_command_factory "github.com/cloudfoundry-incubator/ltc/app_runner/command_factory"
    28  )
    29  
    30  var knownBuildpacks map[string]string
    31  
    32  func init() {
    33  	knownBuildpacks = map[string]string{
    34  		"go":         "https://github.com/cloudfoundry/go-buildpack.git",
    35  		"java":       "https://github.com/cloudfoundry/java-buildpack.git",
    36  		"python":     "https://github.com/cloudfoundry/python-buildpack.git",
    37  		"ruby":       "https://github.com/cloudfoundry/ruby-buildpack.git",
    38  		"nodejs":     "https://github.com/cloudfoundry/nodejs-buildpack.git",
    39  		"php":        "https://github.com/cloudfoundry/php-buildpack.git",
    40  		"binary":     "https://github.com/cloudfoundry/binary-buildpack.git",
    41  		"staticfile": "https://github.com/cloudfoundry/staticfile-buildpack.git",
    42  	}
    43  }
    44  
    45  type DropletRunnerCommandFactory struct {
    46  	app_runner_command_factory.AppRunnerCommandFactory
    47  
    48  	blobStoreVerifier BlobStoreVerifier
    49  	taskExaminer      task_examiner.TaskExaminer
    50  	dropletRunner     droplet_runner.DropletRunner
    51  	cfIgnore          cf_ignore.CFIgnore
    52  	zipper            zipper.Zipper
    53  	config            *config_package.Config
    54  }
    55  
    56  //go:generate counterfeiter -o fake_blob_store_verifier/fake_blob_store_verifier.go . BlobStoreVerifier
    57  type BlobStoreVerifier interface {
    58  	Verify(config *config_package.Config) (authorized bool, err error)
    59  }
    60  
    61  type dropletSliceSortedByCreated []droplet_runner.Droplet
    62  
    63  func (ds dropletSliceSortedByCreated) Len() int { return len(ds) }
    64  func (ds dropletSliceSortedByCreated) Less(i, j int) bool {
    65  	if ds[j].Created.IsZero() {
    66  		return false
    67  	} else if ds[i].Created.IsZero() {
    68  		return true
    69  	} else {
    70  		return ds[j].Created.Before(ds[i].Created)
    71  	}
    72  }
    73  func (ds dropletSliceSortedByCreated) Swap(i, j int) { ds[i], ds[j] = ds[j], ds[i] }
    74  
    75  func NewDropletRunnerCommandFactory(appRunnerCommandFactory app_runner_command_factory.AppRunnerCommandFactory, blobStoreVerifier BlobStoreVerifier, taskExaminer task_examiner.TaskExaminer, dropletRunner droplet_runner.DropletRunner, cfIgnore cf_ignore.CFIgnore, zipper zipper.Zipper, config *config_package.Config) *DropletRunnerCommandFactory {
    76  	return &DropletRunnerCommandFactory{
    77  		AppRunnerCommandFactory: appRunnerCommandFactory,
    78  		blobStoreVerifier:       blobStoreVerifier,
    79  		taskExaminer:            taskExaminer,
    80  		dropletRunner:           dropletRunner,
    81  		cfIgnore:                cfIgnore,
    82  		zipper:                  zipper,
    83  		config:                  config,
    84  	}
    85  }
    86  
    87  func (factory *DropletRunnerCommandFactory) MakeListDropletsCommand() cli.Command {
    88  	var listDropletsCommand = cli.Command{
    89  		Name:        "list-droplets",
    90  		Aliases:     []string{"lsd"},
    91  		Usage:       "Lists the droplets in the droplet store",
    92  		Description: "ltc list-droplets",
    93  		Action:      factory.listDroplets,
    94  	}
    95  
    96  	return listDropletsCommand
    97  }
    98  
    99  func (factory *DropletRunnerCommandFactory) MakeBuildDropletCommand() cli.Command {
   100  	var launchFlags = []cli.Flag{
   101  		cli.StringFlag{
   102  			Name:  "path, p",
   103  			Usage: "Path to droplet source",
   104  			Value: ".",
   105  		},
   106  		cli.IntFlag{
   107  			Name:  "cpu-weight, c",
   108  			Usage: "Relative CPU weight for the container (valid values: 1-100)",
   109  			Value: 100,
   110  		},
   111  		cli.IntFlag{
   112  			Name:  "memory-mb, m",
   113  			Usage: "Memory limit for container in MB",
   114  			Value: 1024,
   115  		},
   116  		cli.IntFlag{
   117  			Name:  "disk-mb, d",
   118  			Usage: "Disk limit for container in MB",
   119  			Value: 0,
   120  		},
   121  		cli.StringSliceFlag{
   122  			Name:  "env, e",
   123  			Usage: "Environment variables (can be passed multiple times)",
   124  			Value: &cli.StringSlice{},
   125  		},
   126  		cli.DurationFlag{
   127  			Name:  "timeout, t",
   128  			Usage: "Polling timeout for app to start",
   129  			Value: app_runner_command_factory.DefaultPollingTimeout,
   130  		},
   131  	}
   132  
   133  	var buildDropletCommand = cli.Command{
   134  		Name:        "build-droplet",
   135  		Aliases:     []string{"bd"},
   136  		Usage:       "Builds app bits into a droplet using a CF buildpack",
   137  		Description: "ltc build-droplet <droplet-name> <buildpack-uri>",
   138  		Action:      factory.buildDroplet,
   139  		Flags:       launchFlags,
   140  	}
   141  
   142  	return buildDropletCommand
   143  }
   144  
   145  func (factory *DropletRunnerCommandFactory) MakeLaunchDropletCommand() cli.Command {
   146  	var launchFlags = []cli.Flag{
   147  		cli.StringSliceFlag{
   148  			Name:  "env, e",
   149  			Usage: "Environment variables (can be passed multiple times)",
   150  			Value: &cli.StringSlice{},
   151  		},
   152  		cli.IntFlag{
   153  			Name:  "cpu-weight, c",
   154  			Usage: "Relative CPU weight for the container (valid values: 1-100)",
   155  			Value: 100,
   156  		},
   157  		cli.IntFlag{
   158  			Name:  "memory-mb, m",
   159  			Usage: "Memory limit for container in MB",
   160  			Value: 128,
   161  		},
   162  		cli.IntFlag{
   163  			Name:  "disk-mb, d",
   164  			Usage: "Disk limit for container in MB",
   165  			Value: 0,
   166  		},
   167  		cli.StringFlag{
   168  			Name:  "ports, p",
   169  			Usage: "Ports to expose on the container (comma delimited)",
   170  		},
   171  		cli.IntFlag{
   172  			Name:  "monitor-port, M",
   173  			Usage: "Selects the port used to healthcheck the app",
   174  		},
   175  		cli.StringFlag{
   176  			Name: "monitor-url, U",
   177  			Usage: "Uses HTTP to healthcheck the app\n\t\t" +
   178  				"format is: <port>:<endpoint-path>",
   179  		},
   180  		cli.DurationFlag{
   181  			Name:  "monitor-timeout",
   182  			Usage: "Timeout for the app healthcheck",
   183  			Value: time.Second,
   184  		},
   185  		cli.StringSliceFlag{
   186  			Name:  "http-route, R",
   187  			Usage: "Requests for <host> on port 80 will be forwarded to the associated container port. Container ports must be among those specified with --ports or with the EXPOSE Docker image directive. Usage: --http-route <host>:<container-port>. Can be passed multiple times.",
   188  		},
   189  		cli.StringSliceFlag{
   190  			Name:  "tcp-route, T",
   191  			Usage: "Requests for the provided external port will be forwarded to the associated container port. Container ports must be among those specified with --ports or with the EXPOSE Docker image directive. Usage: --tcp-route <external-port>:<container-port>. Can be passed multiple times.",
   192  		},
   193  		cli.IntFlag{
   194  			Name:  "instances, i",
   195  			Usage: "Number of application instances to spawn on launch",
   196  			Value: 1,
   197  		},
   198  		cli.BoolFlag{
   199  			Name:  "no-monitor",
   200  			Usage: "Disables healthchecking for the app",
   201  		},
   202  		cli.BoolFlag{
   203  			Name:  "no-routes",
   204  			Usage: "Registers no routes for the app",
   205  		},
   206  		cli.DurationFlag{
   207  			Name:  "timeout, t",
   208  			Usage: "Polling timeout for app to start",
   209  			Value: app_runner_command_factory.DefaultPollingTimeout,
   210  		},
   211  		cli.StringFlag{
   212  			Name:  "http-routes",
   213  			Usage: "DEPRECATED: Please use --http-route instead.",
   214  		},
   215  		cli.StringFlag{
   216  			Name:  "tcp-routes",
   217  			Usage: "DEPRECATED: Please use --tcp-route instead.",
   218  		},
   219  	}
   220  
   221  	var launchDropletCommand = cli.Command{
   222  		Name:    "launch-droplet",
   223  		Aliases: []string{"ld"},
   224  		Usage:   "Launches a droplet as an app running on lattice",
   225  		Description: `ltc launch-droplet <app-name> <droplet-name>
   226  
   227     To provide a custom command:
   228     ltc launch-droplet <app-name> <droplet-name> [<optional flags>] -- <start-command> <start-command-arg1> <start-command-arg2> ...
   229  
   230     Two http routes are created by default, both routing to container port 8080. E.g. for application myapp:
   231       - requests to myapp.<system-domain>:80 will be routed to container port 8080
   232       - requests to myapp-8080.<system-domain>:80 will be routed to container port 8080
   233  
   234     To configure your own routing:
   235       ltc launch-droplet <app-name> <droplet-name> --http-route <host>:<container-port> [ --http-route <host>:<container-port> ...] --tcp-route <external-port>:<container-port> [ --tcp-route <external-port>:<container-port> ...]
   236  
   237       Examples:
   238         ltc launch-droplet myapp ruby --http-route=myapp-admin:6000 will route requests received at myapp-admin.<system-domain>:80 to container port 6000.
   239         ltc launch-droplet myapp ruby --tcp-route=50000:6379 will route requests received at <system-domain>:50000 to container port 6379.
   240  `,
   241  		Action: factory.launchDroplet,
   242  		Flags:  launchFlags,
   243  	}
   244  
   245  	return launchDropletCommand
   246  }
   247  
   248  func (factory *DropletRunnerCommandFactory) MakeRemoveDropletCommand() cli.Command {
   249  	var removeDropletCommand = cli.Command{
   250  		Name:        "remove-droplet",
   251  		Aliases:     []string{"rd"},
   252  		Usage:       "Removes a droplet from the droplet store",
   253  		Description: "ltc remove-droplet <droplet-name>",
   254  		Action:      factory.removeDroplet,
   255  	}
   256  
   257  	return removeDropletCommand
   258  }
   259  
   260  func (factory *DropletRunnerCommandFactory) MakeImportDropletCommand() cli.Command {
   261  	var importDropletCommand = cli.Command{
   262  		Name:        "import-droplet",
   263  		Aliases:     []string{"id"},
   264  		Usage:       "Imports a droplet from disk to the droplet store",
   265  		Description: "ltc import-droplet <droplet-name> <droplet-path> <metadata-path>",
   266  		Action:      factory.importDroplet,
   267  	}
   268  
   269  	return importDropletCommand
   270  }
   271  
   272  func (factory *DropletRunnerCommandFactory) importDroplet(context *cli.Context) {
   273  	dropletName := context.Args().First()
   274  	dropletPath := context.Args().Get(1)
   275  	if dropletName == "" || dropletPath == "" {
   276  		factory.UI.SayIncorrectUsage("<droplet-name> and <droplet-path> are required")
   277  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   278  		return
   279  	}
   280  
   281  	if err := factory.dropletRunner.ImportDroplet(dropletName, dropletPath); err != nil {
   282  		factory.UI.SayLine(fmt.Sprintf("Error importing %s: %s", dropletName, err))
   283  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   284  		return
   285  	}
   286  	factory.UI.SayLine("Imported " + dropletName)
   287  }
   288  
   289  func (factory *DropletRunnerCommandFactory) MakeExportDropletCommand() cli.Command {
   290  	var exportDropletCommand = cli.Command{
   291  		Name:        "export-droplet",
   292  		Aliases:     []string{"ed"},
   293  		Usage:       "Exports a droplet from the droplet store to disk",
   294  		Description: "ltc export-droplet <droplet-name>",
   295  		Action:      factory.exportDroplet,
   296  	}
   297  
   298  	return exportDropletCommand
   299  }
   300  
   301  func (factory *DropletRunnerCommandFactory) listDroplets(context *cli.Context) {
   302  	if !factory.ensureBlobStoreVerified() {
   303  		return
   304  	}
   305  
   306  	droplets, err := factory.dropletRunner.ListDroplets()
   307  	if err != nil {
   308  		factory.UI.SayLine(fmt.Sprintf("Error listing droplets: %s", err))
   309  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   310  		return
   311  	}
   312  
   313  	sort.Sort(dropletSliceSortedByCreated(droplets))
   314  
   315  	w := &tabwriter.Writer{}
   316  	w.Init(factory.UI, 12, 8, 1, '\t', 0)
   317  
   318  	fmt.Fprintln(w, "Droplet\tCreated At\tSize")
   319  	for _, droplet := range droplets {
   320  		size := bytefmt.ByteSize(uint64(droplet.Size))
   321  		if !droplet.Created.IsZero() {
   322  			fmt.Fprintf(w, "%s\t%s\t%s\n", droplet.Name, droplet.Created.Format("01/02 15:04:05.00"), size)
   323  		} else {
   324  			fmt.Fprintf(w, "%s\t\t%s\n", droplet.Name, size)
   325  		}
   326  	}
   327  
   328  	w.Flush()
   329  }
   330  
   331  func (factory *DropletRunnerCommandFactory) ensureBlobStoreVerified() bool {
   332  	authorized, err := factory.blobStoreVerifier.Verify(factory.config)
   333  	if err != nil {
   334  		factory.UI.SayLine("Error verifying droplet store: " + err.Error())
   335  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   336  		return false
   337  	}
   338  	if !authorized {
   339  		factory.UI.SayLine("Error verifying droplet store: unauthorized")
   340  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   341  		return false
   342  	}
   343  	return true
   344  }
   345  
   346  func (factory *DropletRunnerCommandFactory) buildDroplet(context *cli.Context) {
   347  	pathFlag := context.String("path")
   348  	cpuWeightFlag := context.Int("cpu-weight")
   349  	memoryMBFlag := context.Int("memory-mb")
   350  	diskMBFlag := context.Int("disk-mb")
   351  	envFlag := context.StringSlice("env")
   352  	timeoutFlag := context.Duration("timeout")
   353  	dropletName := context.Args().First()
   354  	buildpack := context.Args().Get(1)
   355  
   356  	if dropletName == "" || buildpack == "" {
   357  		factory.UI.SayIncorrectUsage("<droplet-name> and <buildpack-uri> are required")
   358  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   359  		return
   360  	}
   361  
   362  	var buildpackUrl string
   363  	if knownBuildpackUrl, ok := knownBuildpacks[buildpack]; ok {
   364  		buildpackUrl = knownBuildpackUrl
   365  	} else if _, err := url.ParseRequestURI(buildpack); err == nil {
   366  		buildpackUrl = buildpack
   367  	} else {
   368  		factory.UI.SayIncorrectUsage(fmt.Sprintf("invalid buildpack %s", buildpack))
   369  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   370  		return
   371  	}
   372  
   373  	if cpuWeightFlag < 1 || cpuWeightFlag > 100 {
   374  		factory.UI.SayIncorrectUsage("invalid CPU Weight")
   375  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   376  		return
   377  	}
   378  
   379  	if !factory.ensureBlobStoreVerified() {
   380  		return
   381  	}
   382  
   383  	var archivePath string
   384  	var err error
   385  
   386  	if factory.zipper.IsZipFile(pathFlag) {
   387  		tmpDir, err := ioutil.TempDir("", "rezip")
   388  		if err != nil {
   389  			factory.UI.SayLine(fmt.Sprintf("Error re-archiving %s: %s", pathFlag, err))
   390  			factory.ExitHandler.Exit(exit_codes.FileSystemError)
   391  			return
   392  		}
   393  		defer os.RemoveAll(tmpDir)
   394  
   395  		if err := factory.zipper.Unzip(pathFlag, tmpDir); err != nil {
   396  			factory.UI.SayLine(fmt.Sprintf("Error unarchiving %s: %s", pathFlag, err))
   397  			factory.ExitHandler.Exit(exit_codes.FileSystemError)
   398  			return
   399  		}
   400  
   401  		archivePath, err = factory.zipper.Zip(tmpDir, factory.cfIgnore)
   402  		if err != nil {
   403  			factory.UI.SayLine(fmt.Sprintf("Error re-archiving %s: %s", pathFlag, err))
   404  			factory.ExitHandler.Exit(exit_codes.FileSystemError)
   405  			return
   406  		}
   407  		defer os.Remove(archivePath)
   408  	} else {
   409  		archivePath, err = factory.zipper.Zip(pathFlag, factory.cfIgnore)
   410  		if err != nil {
   411  			factory.UI.SayLine(fmt.Sprintf("Error archiving %s: %s", pathFlag, err))
   412  			factory.ExitHandler.Exit(exit_codes.FileSystemError)
   413  			return
   414  		}
   415  		defer os.Remove(archivePath)
   416  	}
   417  
   418  	factory.UI.SayLine("Uploading application bits...")
   419  
   420  	if err := factory.dropletRunner.UploadBits(dropletName, archivePath); err != nil {
   421  		factory.UI.SayLine(fmt.Sprintf("Error uploading %s: %s", dropletName, err))
   422  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   423  		return
   424  	}
   425  
   426  	factory.UI.SayLine("Uploaded.")
   427  
   428  	environment := factory.AppRunnerCommandFactory.BuildEnvironment(envFlag)
   429  
   430  	taskName := "build-droplet-" + dropletName
   431  	if err := factory.dropletRunner.BuildDroplet(taskName, dropletName, buildpackUrl, environment, memoryMBFlag, cpuWeightFlag, diskMBFlag); err != nil {
   432  		factory.UI.SayLine(fmt.Sprintf("Error submitting build of %s: %s", dropletName, err))
   433  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   434  		return
   435  	}
   436  
   437  	factory.UI.SayLine("Submitted build of " + dropletName)
   438  
   439  	go factory.TailedLogsOutputter.OutputTailedLogs(taskName)
   440  	defer factory.TailedLogsOutputter.StopOutputting()
   441  
   442  	ok, taskState := factory.waitForBuildTask(timeoutFlag, taskName)
   443  	if ok {
   444  		if taskState.Failed {
   445  			factory.UI.SayLine("Build failed: " + taskState.FailureReason)
   446  			factory.ExitHandler.Exit(exit_codes.CommandFailed)
   447  		} else {
   448  			factory.UI.SayLine("Build completed")
   449  		}
   450  	} else {
   451  		factory.UI.SayLine(colors.Red("Timed out waiting for the build to complete."))
   452  		factory.UI.SayLine("Lattice is still building your application in the background.")
   453  
   454  		factory.UI.SayLine(fmt.Sprintf("To view logs:\n\tltc logs %s", taskName))
   455  		factory.UI.SayLine(fmt.Sprintf("To view status:\n\tltc status %s", taskName))
   456  		factory.UI.SayNewLine()
   457  	}
   458  }
   459  
   460  func (factory *DropletRunnerCommandFactory) waitForBuildTask(pollTimeout time.Duration, taskName string) (bool, task_examiner.TaskInfo) {
   461  	var taskInfo task_examiner.TaskInfo
   462  	ok := factory.pollUntilSuccess(pollTimeout, func() bool {
   463  		var err error
   464  		taskInfo, err = factory.taskExaminer.TaskStatus(taskName)
   465  		if err != nil {
   466  			factory.UI.SayLine(colors.Red("Error requesting task status: %s"), err)
   467  			return true
   468  		}
   469  
   470  		return taskInfo.State != "RUNNING" && taskInfo.State != "PENDING"
   471  	})
   472  
   473  	return ok, taskInfo
   474  }
   475  
   476  func (factory *DropletRunnerCommandFactory) pollUntilSuccess(pollTimeout time.Duration, pollingFunc func() bool) (ok bool) {
   477  	startingTime := factory.Clock.Now()
   478  	for startingTime.Add(pollTimeout).After(factory.Clock.Now()) {
   479  		if result := pollingFunc(); result {
   480  			return true
   481  		}
   482  
   483  		factory.Clock.Sleep(1 * time.Second)
   484  	}
   485  	return false
   486  }
   487  
   488  func (factory *DropletRunnerCommandFactory) launchDroplet(context *cli.Context) {
   489  	envVarsFlag := context.StringSlice("env")
   490  	instancesFlag := context.Int("instances")
   491  	cpuWeightFlag := uint(context.Int("cpu-weight"))
   492  	memoryMBFlag := context.Int("memory-mb")
   493  	diskMBFlag := context.Int("disk-mb")
   494  	portsFlag := context.String("ports")
   495  	noMonitorFlag := context.Bool("no-monitor")
   496  	portMonitorFlag := context.Int("monitor-port")
   497  	urlMonitorFlag := context.String("monitor-url")
   498  	monitorTimeoutFlag := context.Duration("monitor-timeout")
   499  	httpRouteFlag := context.StringSlice("http-route")
   500  	tcpRouteFlag := context.StringSlice("tcp-route")
   501  	noRoutesFlag := context.Bool("no-routes")
   502  	timeoutFlag := context.Duration("timeout")
   503  	appName := context.Args().Get(0)
   504  	dropletName := context.Args().Get(1)
   505  	terminator := context.Args().Get(2)
   506  	startCommand := context.Args().Get(3)
   507  
   508  	var startArgs []string
   509  
   510  	switch {
   511  	case len(context.Args()) < 2:
   512  		factory.UI.SayIncorrectUsage("<app-name> and <droplet-name> are required")
   513  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   514  		return
   515  	case startCommand != "" && terminator != "--":
   516  		factory.UI.SayIncorrectUsage("'--' Required before start command")
   517  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   518  		return
   519  	case len(context.Args()) > 4:
   520  		startArgs = context.Args()[4:]
   521  	case cpuWeightFlag < 1 || cpuWeightFlag > 100:
   522  		factory.UI.SayIncorrectUsage("invalid CPU Weight")
   523  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   524  		return
   525  	}
   526  
   527  	httpRoutesFlag := context.String("http-routes")
   528  	if httpRoutesFlag != "" {
   529  		factory.UI.SayIncorrectUsage("Unable to parse routes\n  Pass multiple --http-route flags instead of comma-delimiting.  See help page for details.")
   530  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   531  		return
   532  	}
   533  
   534  	tcpRoutesFlag := context.String("tcp-routes")
   535  	if tcpRoutesFlag != "" {
   536  		factory.UI.SayIncorrectUsage("Unable to parse routes\n  Pass multiple --tcp-route flags instead of comma-delimiting.  See help page for details.")
   537  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   538  		return
   539  	}
   540  
   541  	exposedPorts, err := factory.parsePortsFromArgs(portsFlag)
   542  	if err != nil {
   543  		factory.UI.SayLine(err.Error())
   544  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   545  		return
   546  	}
   547  
   548  	monitorConfig, err := factory.GetMonitorConfig(exposedPorts, portMonitorFlag, noMonitorFlag, urlMonitorFlag, "", monitorTimeoutFlag)
   549  	if err != nil {
   550  		factory.UI.SayLine(err.Error())
   551  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   552  		return
   553  	}
   554  
   555  	routeOverrides, err := factory.ParseRouteOverrides(httpRouteFlag, exposedPorts)
   556  	if err != nil {
   557  		factory.UI.SayLine(err.Error())
   558  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   559  		return
   560  	}
   561  
   562  	tcpRoutes, err := factory.ParseTcpRoutes(tcpRouteFlag, exposedPorts)
   563  	if err != nil {
   564  		factory.UI.SayLine(err.Error())
   565  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   566  		return
   567  	}
   568  
   569  	appEnvironmentParams := app_runner.AppEnvironmentParams{
   570  		EnvironmentVariables: factory.BuildAppEnvironment(envVarsFlag, appName),
   571  		Privileged:           false,
   572  		User:                 "vcap",
   573  		Monitor:              monitorConfig,
   574  		Instances:            instancesFlag,
   575  		CPUWeight:            cpuWeightFlag,
   576  		MemoryMB:             memoryMBFlag,
   577  		DiskMB:               diskMBFlag,
   578  		ExposedPorts:         exposedPorts,
   579  		RouteOverrides:       routeOverrides,
   580  		TcpRoutes:            tcpRoutes,
   581  		NoRoutes:             noRoutesFlag,
   582  	}
   583  
   584  	appEnvironmentParams.EnvironmentVariables["MEMORY_LIMIT"] = fmt.Sprintf("%dM", memoryMBFlag)
   585  
   586  	if err := factory.dropletRunner.LaunchDroplet(appName, dropletName, startCommand, startArgs, appEnvironmentParams); err != nil {
   587  		factory.UI.SayLine(fmt.Sprintf("Error launching app %s from droplet %s: %s", appName, dropletName, err))
   588  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   589  		return
   590  	}
   591  
   592  	factory.WaitForAppCreation(appName, timeoutFlag, instancesFlag)
   593  }
   594  
   595  func (factory *DropletRunnerCommandFactory) removeDroplet(context *cli.Context) {
   596  	dropletName := context.Args().First()
   597  	if dropletName == "" {
   598  		factory.UI.SayIncorrectUsage("<droplet-name> is required")
   599  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   600  		return
   601  	}
   602  
   603  	err := factory.dropletRunner.RemoveDroplet(dropletName)
   604  	if err != nil {
   605  		factory.UI.SayLine(fmt.Sprintf("Error removing droplet %s: %s", dropletName, err))
   606  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   607  		return
   608  	}
   609  
   610  	factory.UI.SayLine("Droplet removed")
   611  }
   612  
   613  func (factory *DropletRunnerCommandFactory) exportDroplet(context *cli.Context) {
   614  	dropletName := context.Args().First()
   615  	if dropletName == "" {
   616  		factory.UI.SayIncorrectUsage("<droplet-name> is required")
   617  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   618  		return
   619  	}
   620  
   621  	dropletReader, err := factory.dropletRunner.ExportDroplet(dropletName)
   622  	if err != nil {
   623  		factory.UI.SayLine(fmt.Sprintf("Error exporting droplet %s: %s", dropletName, err))
   624  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   625  		return
   626  	}
   627  	defer dropletReader.Close()
   628  
   629  	dropletPath := dropletName + ".tgz"
   630  
   631  	dropletWriter, err := os.OpenFile(dropletPath, os.O_WRONLY|os.O_CREATE, os.FileMode(0644))
   632  	if err != nil {
   633  		factory.UI.SayLine(fmt.Sprintf("Error exporting droplet '%s' to %s: %s", dropletName, dropletPath, err))
   634  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   635  		return
   636  	}
   637  	defer dropletWriter.Close()
   638  
   639  	_, err = io.Copy(dropletWriter, dropletReader)
   640  	if err != nil {
   641  		factory.UI.SayLine(fmt.Sprintf("Error exporting droplet '%s' to %s: %s", dropletName, dropletPath, err))
   642  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   643  		return
   644  	}
   645  
   646  	factory.UI.SayLine(fmt.Sprintf("Droplet '%s' exported to %s.", dropletName, dropletPath))
   647  }
   648  
   649  func (factory *DropletRunnerCommandFactory) parsePortsFromArgs(portsFlag string) ([]uint16, error) {
   650  	if portsFlag != "" {
   651  		portStrings := strings.Split(portsFlag, ",")
   652  		sort.Strings(portStrings)
   653  
   654  		convertedPorts := []uint16{}
   655  		for _, p := range portStrings {
   656  			intPort, err := strconv.Atoi(p)
   657  			if err != nil || intPort > 65535 {
   658  				return []uint16{}, errors.New(app_runner_command_factory.InvalidPortErrorMessage)
   659  			}
   660  			convertedPorts = append(convertedPorts, uint16(intPort))
   661  		}
   662  		return convertedPorts, nil
   663  	}
   664  
   665  	factory.UI.SayLine(fmt.Sprintf("No port specified. Defaulting to 8080."))
   666  
   667  	return []uint16{8080}, nil
   668  }