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 }