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 }