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

     1  package command_factory
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/cloudfoundry-incubator/ltc/app_examiner"
    12  	"github.com/cloudfoundry-incubator/ltc/app_runner"
    13  	"github.com/cloudfoundry-incubator/ltc/app_runner/command_factory"
    14  	"github.com/cloudfoundry-incubator/ltc/docker_runner/docker_metadata_fetcher"
    15  	"github.com/cloudfoundry-incubator/ltc/docker_runner/docker_repository_name_formatter"
    16  	"github.com/cloudfoundry-incubator/ltc/exit_handler"
    17  	"github.com/cloudfoundry-incubator/ltc/exit_handler/exit_codes"
    18  	"github.com/cloudfoundry-incubator/ltc/logs/console_tailed_logs_outputter"
    19  	"github.com/cloudfoundry-incubator/ltc/terminal"
    20  	"github.com/codegangsta/cli"
    21  	"github.com/pivotal-golang/clock"
    22  	"github.com/pivotal-golang/lager"
    23  )
    24  
    25  type DockerRunnerCommandFactory struct {
    26  	command_factory.AppRunnerCommandFactory
    27  
    28  	dockerMetadataFetcher docker_metadata_fetcher.DockerMetadataFetcher
    29  }
    30  
    31  type DockerRunnerCommandFactoryConfig struct {
    32  	AppRunner           app_runner.AppRunner
    33  	AppExaminer         app_examiner.AppExaminer
    34  	UI                  terminal.UI
    35  	Domain              string
    36  	Env                 []string
    37  	Clock               clock.Clock
    38  	Logger              lager.Logger
    39  	ExitHandler         exit_handler.ExitHandler
    40  	TailedLogsOutputter console_tailed_logs_outputter.TailedLogsOutputter
    41  
    42  	DockerMetadataFetcher docker_metadata_fetcher.DockerMetadataFetcher
    43  }
    44  
    45  func NewDockerRunnerCommandFactory(config DockerRunnerCommandFactoryConfig) *DockerRunnerCommandFactory {
    46  	return &DockerRunnerCommandFactory{
    47  		AppRunnerCommandFactory: command_factory.AppRunnerCommandFactory{
    48  			AppRunner:           config.AppRunner,
    49  			AppExaminer:         config.AppExaminer,
    50  			UI:                  config.UI,
    51  			Domain:              config.Domain,
    52  			Env:                 config.Env,
    53  			Clock:               config.Clock,
    54  			ExitHandler:         config.ExitHandler,
    55  			TailedLogsOutputter: config.TailedLogsOutputter,
    56  		},
    57  
    58  		dockerMetadataFetcher: config.DockerMetadataFetcher,
    59  	}
    60  }
    61  
    62  func (factory *DockerRunnerCommandFactory) MakeCreateAppCommand() cli.Command {
    63  
    64  	var createFlags = []cli.Flag{
    65  		cli.StringFlag{
    66  			Name:  "working-dir, w",
    67  			Usage: "Working directory for container (overrides Docker metadata)",
    68  			Value: "",
    69  		},
    70  		cli.StringSliceFlag{
    71  			Name:  "env, e",
    72  			Usage: "Environment variables (can be passed multiple times)",
    73  			Value: &cli.StringSlice{},
    74  		},
    75  		cli.IntFlag{
    76  			Name:  "cpu-weight, c",
    77  			Usage: "Relative CPU weight for the container (valid values: 1-100)",
    78  			Value: 100,
    79  		},
    80  		cli.IntFlag{
    81  			Name:  "memory-mb, m",
    82  			Usage: "Memory limit for container in MB",
    83  			Value: 128,
    84  		},
    85  		cli.IntFlag{
    86  			Name:  "disk-mb, d",
    87  			Usage: "Disk limit for container in MB",
    88  			Value: 0,
    89  		},
    90  		cli.StringFlag{
    91  			Name:  "user, u",
    92  			Usage: "Runs the app under this user context",
    93  		},
    94  		cli.BoolFlag{
    95  			Name:  "run-as-root, r",
    96  			Usage: "Deprecated: please use --user instead",
    97  		},
    98  		cli.BoolFlag{
    99  			Name:  "privileged",
   100  			Usage: "Run the app in a privileged container (Warning: This is insecure.)",
   101  		},
   102  		cli.StringFlag{
   103  			Name:  "ports, p",
   104  			Usage: "Ports to expose on the container (comma delimited)",
   105  		},
   106  		cli.IntFlag{
   107  			Name:  "monitor-port, M",
   108  			Usage: "Selects the port used to healthcheck the app",
   109  		},
   110  		cli.StringFlag{
   111  			Name: "monitor-url, U",
   112  			Usage: "Uses HTTP to healthcheck the app\n\t\t" +
   113  				"format is: <port>:<endpoint-path>",
   114  		},
   115  		cli.DurationFlag{
   116  			Name:  "monitor-timeout",
   117  			Usage: "Timeout for the app healthcheck",
   118  			Value: time.Second,
   119  		},
   120  		cli.StringFlag{
   121  			Name:  "monitor-command",
   122  			Usage: "Uses the command (with arguments) to healthcheck the app",
   123  		},
   124  		cli.StringSliceFlag{
   125  			Name:  "http-route, R",
   126  			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.",
   127  		},
   128  		cli.StringSliceFlag{
   129  			Name:  "tcp-route, T",
   130  			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.",
   131  		},
   132  		cli.IntFlag{
   133  			Name:  "instances, i",
   134  			Usage: "Number of application instances to spawn on launch",
   135  			Value: 1,
   136  		},
   137  		cli.BoolFlag{
   138  			Name:  "no-monitor",
   139  			Usage: "Disables healthchecking for the app",
   140  		},
   141  		cli.BoolFlag{
   142  			Name:  "no-routes",
   143  			Usage: "Registers no routes for the app",
   144  		},
   145  		cli.DurationFlag{
   146  			Name:  "timeout, t",
   147  			Usage: "Polling timeout for app to start",
   148  			Value: command_factory.DefaultPollingTimeout,
   149  		},
   150  		cli.StringFlag{
   151  			Name:  "http-routes",
   152  			Usage: "DEPRECATED: Please use --http-route instead.",
   153  		},
   154  		cli.StringFlag{
   155  			Name:  "tcp-routes",
   156  			Usage: "DEPRECATED: Please use --tcp-route instead.",
   157  		},
   158  	}
   159  
   160  	var createAppCommand = cli.Command{
   161  		Name:    "create",
   162  		Aliases: []string{"cr"},
   163  		Usage:   "Creates a docker app on lattice",
   164  		Description: `ltc create <app-name> <docker-image>
   165  
   166     <app-name> is required and must be unique across the Lattice cluster
   167     <docker-image> is required and must match the standard docker image format
   168     e.g.
   169     		1. "cloudfoundry/lattice-app"
   170     		2. "redis" - for official images; resolves to library/redis
   171  
   172     ltc will fetch the command associated with your Docker image.
   173     To provide a custom command:
   174     ltc create <app-name> <docker-image> <optional flags> -- <start-command> <start-command-arg1> <start-command-arg2> ...
   175  
   176     ltc will also fetch the working directory associated with your Docker image.
   177     If the image does not specify a working directory, ltc will default the working directory to "/"
   178     To provide a custom working directory:
   179     ltc create <app-name> <docker-image> --working-dir=<working-dir> -- <start-command> <start-command-arg1> <start-command-arg2> ...
   180  
   181     To specify environment variables:
   182     ltc create <app-name> <docker-image> -e FOO=<foo> -e BAZ=<baz>
   183  
   184     By default, http routes will be created for all container ports specified in the EXPOSE directive in
   185     the Docker image. E.g. for application myapp and a Docker image that specifies ports 80 and 8080,
   186     two http routes will be created by default:
   187  
   188       - requests to myapp.<system-domain>:80 will be routed to container port 80
   189       - requests to myapp-8080.<system-domain>:80 will be routed to container port 8080
   190  
   191     To configure your own routing:
   192     ltc create <app-name> <docker-image> --http-route <host>:<container-port> [ --http-route <host>:<container-port> ...] --tcp-route <external-port>:<container-port> [ --tcp-route <external-port>:<container-port> ...]
   193  ]
   194  
   195     Examples:
   196       ltc create myapp mydockerimage --http-route=myapp-admin:6000 will route requests received at myapp-admin.<system-domain>:80 to container port 6000.
   197       ltc create myredis redis --tcp-route=50000:6379 will route requests received at <system-domain>:50000 to container port 6379.
   198  `,
   199  		Action: factory.createApp,
   200  		Flags:  createFlags,
   201  	}
   202  
   203  	return createAppCommand
   204  }
   205  
   206  func (factory *DockerRunnerCommandFactory) createApp(context *cli.Context) {
   207  	workingDirFlag := context.String("working-dir")
   208  	envVarsFlag := context.StringSlice("env")
   209  	instancesFlag := context.Int("instances")
   210  	cpuWeightFlag := uint(context.Int("cpu-weight"))
   211  	memoryMBFlag := context.Int("memory-mb")
   212  	diskMBFlag := context.Int("disk-mb")
   213  	userFlag := context.String("user")
   214  	runAsRootFlag := context.Bool("run-as-root")
   215  	privilegedFlag := context.Bool("privileged")
   216  	portsFlag := context.String("ports")
   217  	noMonitorFlag := context.Bool("no-monitor")
   218  	portMonitorFlag := context.Int("monitor-port")
   219  	urlMonitorFlag := context.String("monitor-url")
   220  	monitorTimeoutFlag := context.Duration("monitor-timeout")
   221  	monitorCommandFlag := context.String("monitor-command")
   222  	httpRouteFlag := context.StringSlice("http-route")
   223  	tcpRouteFlag := context.StringSlice("tcp-route")
   224  	noRoutesFlag := context.Bool("no-routes")
   225  	timeoutFlag := context.Duration("timeout")
   226  	name := context.Args().Get(0)
   227  	dockerPath := context.Args().Get(1)
   228  	terminator := context.Args().Get(2)
   229  	startCommand := context.Args().Get(3)
   230  
   231  	var appArgs []string
   232  	switch {
   233  	case len(context.Args()) < 2:
   234  		factory.UI.SayIncorrectUsage("<app-name> and <docker-image> are required")
   235  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   236  		return
   237  	case startCommand != "" && terminator != "--":
   238  		factory.UI.SayIncorrectUsage("'--' Required before start command")
   239  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   240  		return
   241  	case len(context.Args()) > 4:
   242  		appArgs = context.Args()[4:]
   243  	case cpuWeightFlag < 1 || cpuWeightFlag > 100:
   244  		factory.UI.SayIncorrectUsage("Invalid CPU Weight")
   245  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   246  		return
   247  	}
   248  
   249  	httpRoutesFlag := context.String("http-routes")
   250  	if httpRoutesFlag != "" {
   251  		factory.UI.SayIncorrectUsage("Unable to parse routes\n  Pass multiple --http-route flags instead of comma-delimiting.  See help page for details.")
   252  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   253  		return
   254  	}
   255  
   256  	tcpRoutesFlag := context.String("tcp-routes")
   257  	if tcpRoutesFlag != "" {
   258  		factory.UI.SayIncorrectUsage("Unable to parse routes\n  Pass multiple --tcp-route flags instead of comma-delimiting.  See help page for details.")
   259  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   260  		return
   261  	}
   262  
   263  	imageMetadata, err := factory.dockerMetadataFetcher.FetchMetadata(dockerPath)
   264  	if err != nil {
   265  		factory.UI.SayLine(fmt.Sprintf("Error fetching image metadata: %s", err))
   266  		factory.ExitHandler.Exit(exit_codes.BadDocker)
   267  		return
   268  	}
   269  
   270  	if privilegedFlag {
   271  		factory.UI.SayLine("Warning: It is possible for a privileged app to break out of its container and access the host OS!")
   272  	}
   273  
   274  	if runAsRootFlag {
   275  		userFlag = "root"
   276  		factory.UI.SayLine("Warning: run-as-root has been deprecated, please use '--user=root' instead)")
   277  	}
   278  
   279  	if userFlag == "" {
   280  		if imageMetadata.User != "" {
   281  			userFlag = imageMetadata.User
   282  			factory.UI.SayLine("Setting the user to %s (obtained from docker image metadata)...", imageMetadata.User)
   283  		} else {
   284  			userFlag = "root"
   285  			factory.UI.SayLine("Warning: No container user specified to run your app, your app will be run as root!")
   286  		}
   287  	} else {
   288  		factory.UI.SayLine("Setting the user to %s from option...", userFlag)
   289  	}
   290  
   291  	exposedPorts, err := factory.getExposedPortsFromArgs(portsFlag, imageMetadata)
   292  	if err != nil {
   293  		factory.UI.SayLine(err.Error())
   294  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   295  		return
   296  	}
   297  
   298  	monitorConfig, err := factory.GetMonitorConfig(exposedPorts, portMonitorFlag, noMonitorFlag, urlMonitorFlag, monitorCommandFlag, monitorTimeoutFlag)
   299  	if err != nil {
   300  		factory.UI.SayLine(err.Error())
   301  		if err.Error() == command_factory.MonitorPortNotExposed {
   302  			factory.ExitHandler.Exit(exit_codes.CommandFailed)
   303  		} else {
   304  			factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   305  		}
   306  		return
   307  	}
   308  
   309  	if workingDirFlag == "" {
   310  		factory.UI.SayLine("No working directory specified, using working directory from the image metadata...")
   311  		if imageMetadata.WorkingDir != "" {
   312  			workingDirFlag = imageMetadata.WorkingDir
   313  			factory.UI.SayLine("Working directory is:")
   314  			factory.UI.SayLine(workingDirFlag)
   315  		} else {
   316  			workingDirFlag = "/"
   317  		}
   318  	}
   319  
   320  	switch {
   321  	case monitorCommandFlag != "":
   322  		factory.UI.SayLine(fmt.Sprintf("Monitoring the app with command %s", monitorConfig.CustomCommand))
   323  	case !noMonitorFlag:
   324  		factory.UI.SayLine(fmt.Sprintf("Monitoring the app on port %d...", monitorConfig.Port))
   325  	default:
   326  		factory.UI.SayLine("No ports will be monitored.")
   327  	}
   328  
   329  	if startCommand == "" {
   330  		if len(imageMetadata.StartCommand) == 0 {
   331  			factory.UI.SayLine("Unable to determine start command from image metadata.")
   332  			factory.ExitHandler.Exit(exit_codes.BadDocker)
   333  			return
   334  		}
   335  
   336  		factory.UI.SayLine("No start command specified, using start command from the image metadata...")
   337  		startCommand = imageMetadata.StartCommand[0]
   338  
   339  		factory.UI.SayLine("Start command is:")
   340  		factory.UI.SayLine(strings.Join(imageMetadata.StartCommand, " "))
   341  
   342  		appArgs = imageMetadata.StartCommand[1:]
   343  	}
   344  
   345  	routeOverrides, err := factory.ParseRouteOverrides(httpRouteFlag, exposedPorts)
   346  	if err != nil {
   347  		factory.UI.SayLine(err.Error())
   348  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   349  		return
   350  	}
   351  
   352  	tcpRoutes, err := factory.ParseTcpRoutes(tcpRouteFlag, exposedPorts)
   353  	if err != nil {
   354  		factory.UI.SayLine(err.Error())
   355  		factory.ExitHandler.Exit(exit_codes.InvalidSyntax)
   356  		return
   357  	}
   358  
   359  	rootFS, err := docker_repository_name_formatter.FormatForReceptor(dockerPath)
   360  	if err != nil {
   361  		factory.UI.SayLine(err.Error())
   362  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   363  		return
   364  	}
   365  
   366  	envVars := map[string]string{}
   367  
   368  	for _, dockerEnv := range imageMetadata.Env {
   369  		split := strings.SplitN(dockerEnv, "=", 2)
   370  		if len(split) == 2 {
   371  			envVars[split[0]] = split[1]
   372  		}
   373  	}
   374  
   375  	appEnvVars := factory.BuildAppEnvironment(envVarsFlag, name)
   376  	for appEnvKey := range appEnvVars {
   377  		envVars[appEnvKey] = appEnvVars[appEnvKey]
   378  	}
   379  
   380  	err = factory.AppRunner.CreateApp(app_runner.CreateAppParams{
   381  		AppEnvironmentParams: app_runner.AppEnvironmentParams{
   382  			EnvironmentVariables: envVars,
   383  			User:                 userFlag,
   384  			Privileged:           privilegedFlag,
   385  			Monitor:              monitorConfig,
   386  			Instances:            instancesFlag,
   387  			CPUWeight:            cpuWeightFlag,
   388  			MemoryMB:             memoryMBFlag,
   389  			DiskMB:               diskMBFlag,
   390  			ExposedPorts:         exposedPorts,
   391  			WorkingDir:           workingDirFlag,
   392  			RouteOverrides:       routeOverrides,
   393  			TcpRoutes:            tcpRoutes,
   394  			NoRoutes:             noRoutesFlag,
   395  		},
   396  
   397  		Name:         name,
   398  		RootFS:       rootFS,
   399  		StartCommand: startCommand,
   400  		AppArgs:      appArgs,
   401  		Timeout:      timeoutFlag,
   402  	})
   403  	if err != nil {
   404  		factory.UI.SayLine(fmt.Sprintf("Error creating app: %s", err))
   405  		factory.ExitHandler.Exit(exit_codes.CommandFailed)
   406  		return
   407  	}
   408  
   409  	factory.WaitForAppCreation(name, timeoutFlag, instancesFlag)
   410  }
   411  
   412  func (factory *DockerRunnerCommandFactory) getExposedPortsFromArgs(portsFlag string, imageMetadata *docker_metadata_fetcher.ImageMetadata) ([]uint16, error) {
   413  	if portsFlag != "" {
   414  		portStrings := strings.Split(portsFlag, ",")
   415  		sort.Strings(portStrings)
   416  
   417  		convertedPorts := []uint16{}
   418  		for _, p := range portStrings {
   419  			intPort, err := strconv.Atoi(p)
   420  			if err != nil || intPort > 65535 {
   421  				return []uint16{}, errors.New(command_factory.InvalidPortErrorMessage)
   422  			}
   423  			convertedPorts = append(convertedPorts, uint16(intPort))
   424  		}
   425  		return convertedPorts, nil
   426  	}
   427  
   428  	if len(imageMetadata.ExposedPorts) > 0 {
   429  		var exposedPortStrings []string
   430  		for _, port := range imageMetadata.ExposedPorts {
   431  			exposedPortStrings = append(exposedPortStrings, strconv.Itoa(int(port)))
   432  		}
   433  		factory.UI.SayLine(fmt.Sprintf("No port specified, using exposed ports from the image metadata.\n\tExposed Ports: %s", strings.Join(exposedPortStrings, ", ")))
   434  		return imageMetadata.ExposedPorts, nil
   435  	}
   436  
   437  	factory.UI.SayLine(fmt.Sprintf("No port specified, image metadata did not contain exposed ports. Defaulting to 8080."))
   438  	return []uint16{8080}, nil
   439  }