Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docker bridge #621

Merged
merged 13 commits into from
Sep 26, 2014
66 changes: 65 additions & 1 deletion docs/docs/native-docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Docker version 1.0.0 or later installed on each slave node.

## Overview

To use the new native container support, add a `container` field to your
To use the native container support, add a `container` field to your
app definition JSON:

```json
Expand Down Expand Up @@ -86,6 +86,70 @@ For convenience, the mount point of the mesos sandbox is available in the
environment as `$MESOS_SANDBOX`. The `$HOME` environment variable is set
by default to the same value as `$MESOS_SANDBOX`.

### Bridged Networking Mode

_Note: Requires Mesos 0.20.1 and Marathon 0.7.1_

Bridged networking makes it easy to run programs that bind to statically
configured ports in Docker containers. Marathon can "bridge" the gap between
the port resource accounting done by Mesos and the host ports that are bound
by Docker.

**Dynamic port mapping:**

Let's begin by taking an example app definition:

```json
{
"id": "bridged-webapp",
"cmd": "python3 -m http.server 8080",
"cpus": 0.5,
"mem": 64.0,
"instances": 2,
"container": {
"type": "DOCKER",
"docker": {
"image": "python:3",
"network": "BRIDGE",
"portMappings": [
{ "containerPort": 8080, "hostPort": 0, "protocol": "tcp"},
{ "containerPort": 161, "hostPort": 0, "protocol": "udp"}
]
}
},
"healthChecks": [
{
"protocol": "HTTP",
"portIndex": 0,
"path": "/",
"gracePeriodSeconds": 5,
"intervalSeconds": 20,
"maxConsecutiveFailures": 3
}
]
}
```

Here `"hostPort": 0` retains the traditional meaning in Marathon, which is "a
random port from the range included in the Mesos resource offer". The resulting
host ports for each task are exposed via the task details in the REST API and
the Marathon web UI.

**Static port mapping:**

It's also possible to specify non-zero host ports. When doing this
you must ensure that the target ports are included in some resource offers!
The Mesos slave announces port resources in the range `[31000-32000]` by
default. This can be overridden; for example to also expose ports in the range
`[8000-9000]`:

```
--resources="ports(*):[8000-9000, 31000-32000]"
```

See the [network configuration](https://docs.docker.com/articles/networking/)
documentation for more details on how Docker handles networking.

### Using a private Docker Repository

To supply credentials to pull from a private repository, add a `.dockercfg` to
Expand Down
105 changes: 54 additions & 51 deletions docs/docs/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,14 +390,12 @@ List all running applications.
**Request:**

{% highlight http %}
GET /v2/apps HTTP/1.1
GET /v2/apps/ HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Host: mesos.vm:8080
User-Agent: HTTPie/0.8.0
{% endhighlight %}

**Response:**
{%endhighlight%}

{% highlight http %}
HTTP/1.1 200 OK
Expand All @@ -408,65 +406,70 @@ Transfer-Encoding: chunked
{
"apps": [
{
"args": null,
"backoffFactor": 1.15,
"backoffSeconds": 1,
"cmd": "env && python3 -m http.server $PORT0",
"constraints": [
[
"hostname",
"UNIQUE"
]
],
"args": null,
"backoffFactor": 1.15,
"backoffSeconds": 1,
"cmd": "python3 -m http.server 8080",
"constraints": [],
"container": {
"docker": {
"image": "python:3"
},
"type": "DOCKER",
"image": "python:3",
"network": "BRIDGE",
"portMappings": [
{
"containerPort": 8080,
"hostPort": 0,
"protocol": "tcp"
},
{
"containerPort": 161,
"hostPort": 0,
"protocol": "udp"
}
]
},
"type": "DOCKER",
"volumes": []
},
"cpus": 0.25,
},
"cpus": 0.5,
"dependencies": [],
"deployments": [
{
"id": "5cd987cd-85ae-4e70-8df7-f1438367d9cb"
}
],
"disk": 0.0,
"env": {},
"executor": "",
"deployments": [],
"disk": 0.0,
"env": {},
"executor": "",
"healthChecks": [
{
"command": null,
"gracePeriodSeconds": 3,
"intervalSeconds": 10,
"maxConsecutiveFailures": 3,
"path": "/",
"portIndex": 0,
"protocol": "HTTP",
"timeoutSeconds": 5
"command": null,
"gracePeriodSeconds": 5,
"intervalSeconds": 20,
"maxConsecutiveFailures": 3,
"path": "/",
"portIndex": 0,
"protocol": "HTTP",
"timeoutSeconds": 20
}
],
"id": "/my-app",
"instances": 2,
"mem": 50.0,
],
"id": "/bridged-webapp",
"instances": 2,
"mem": 64.0,
"ports": [
10000
],
"requirePorts": false,
"storeUrls": [],
"tasksRunning": 1,
"tasksStaged": 0,
10000,
10001
],
"requirePorts": false,
"storeUrls": [],
"tasksRunning": 2,
"tasksStaged": 0,
"upgradeStrategy": {
"minimumHealthCapacity": 0.5
},
"uris": [],
"user": null,
"version": "2014-08-18T22:36:41.451Z"
"minimumHealthCapacity": 1.0
},
"uris": [],
"user": null,
"version": "2014-09-25T02:26:59.256Z"
}
]
}
{% endhighlight %}
{%endhighlight%}

#### GET `/v2/apps/{appId}`

Expand Down
29 changes: 29 additions & 0 deletions examples/bridge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"id": "bridged-webapp",
"cmd": "python3 -m http.server 8080",
"cpus": 0.5,
"mem": 64.0,
"instances": 2,
"container": {
"type": "DOCKER",
"docker": {
"image": "python:3",
"network": "BRIDGE",
"portMappings": [
{ "containerPort": 8080, "hostPort": 0, "protocol": "tcp"},
{ "containerPort": 161, "hostPort": 0, "protocol": "udp"}
]
}
},
"healthChecks": [
{
"protocol": "HTTP",
"portIndex": 0,
"path": "/",
"gracePeriodSeconds": 5,
"intervalSeconds": 20,
"maxConsecutiveFailures": 3
}
]
}

3 changes: 1 addition & 2 deletions project/build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ object Dependency {
// runtime deps versions
val Chaos = "0.5.6"
val JacksonCCM = "0.1.1"
val MesosUtils = "0.20.0-1"
val MesosUtils = "0.20.1-1"
val Akka = "2.2.4"
val Spray = "1.2.1"
val Json4s = "3.2.5"
Expand Down Expand Up @@ -194,4 +194,3 @@ object Dependency {
val akkaTestKit = "com.typesafe.akka" %% "akka-testkit" % V.Akka
}
}

6 changes: 5 additions & 1 deletion src/main/scala/mesosphere/marathon/state/AppDefinition.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ case class AppDefinition(
)
}

def hasDynamicPort = ports.contains(0)
def containerHostPorts(): Option[Seq[Int]] =
container.flatMap(_.docker.map(_.portMappings.map(_.hostPort)))

def hasDynamicPort(): Boolean =
containerHostPorts.getOrElse(ports).contains(0)

def mergeFromProto(bytes: Array[Byte]): AppDefinition = {
val proto = Protos.ServiceDefinition.parseFrom(bytes)
Expand Down
49 changes: 45 additions & 4 deletions src/main/scala/mesosphere/marathon/state/Container.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,54 @@ object Container {
/**
* Docker-specific container parameters.
*/
case class Docker(image: String = "") {
def toProto(): mesos.ContainerInfo.DockerInfo =
mesos.ContainerInfo.DockerInfo.newBuilder.setImage(image).build
case class Docker(
image: String = "",
network: Option[mesos.ContainerInfo.DockerInfo.Network] = None,
portMappings: Seq[Docker.PortMapping] = Nil) {
def toProto(): mesos.ContainerInfo.DockerInfo = {
val builder = mesos.ContainerInfo.DockerInfo.newBuilder.setImage(image)
network foreach builder.setNetwork
builder.addAllPortMappings(portMappings.map(_.toProto).asJava)
builder.build
}
}

object Docker {
def apply(proto: mesos.ContainerInfo.DockerInfo): Docker =
Docker(image = proto.getImage)
Docker(
image = proto.getImage,
if (proto.hasNetwork) Some(proto.getNetwork) else None,
proto.getPortMappingsList.asScala.map(PortMapping.apply)
)

/**
* @param containerPort The container port to expose
* @param hostPort The host port to bind
* @param protocol Layer 4 protocol to expose (i.e. tcp, udp).
*/
case class PortMapping(
containerPort: Int,
hostPort: Int,
protocol: String) {
def toProto(): mesos.ContainerInfo.DockerInfo.PortMapping = {
mesos.ContainerInfo.DockerInfo.PortMapping.newBuilder
.setContainerPort(containerPort)
.setHostPort(hostPort)
.setProtocol(protocol)
.build
}
}

object PortMapping {
def apply(proto: mesos.ContainerInfo.DockerInfo.PortMapping): PortMapping =
PortMapping(
proto.getContainerPort,
proto.getHostPort,
proto.getProtocol
)

}

}

}
44 changes: 36 additions & 8 deletions src/main/scala/mesosphere/marathon/state/GroupManager.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mesosphere.marathon.state

import java.net.URL
import javax.inject.Inject
import java.lang.{ Integer => JInt }
import scala.concurrent.Future
import scala.util.{ Failure, Success }
import scala.collection.mutable
Expand Down Expand Up @@ -158,19 +159,46 @@ class GroupManager @Singleton @Inject() (
private[state] def assignDynamicAppPort(from: Group, to: Group): Group = {
val portRange = Range(config.localPortMin(), config.localPortMax())
var taken = from.transitiveApps.flatMap(_.ports)
def nextGlobalFreePort: Integer = {
val port = portRange.find(!taken.contains(_)).getOrElse(throw new PortRangeExhaustedException(config.localPortMin(), config.localPortMax()))

def nextGlobalFreePort: JInt = synchronized {
val port = portRange.find(!taken.contains(_))
.getOrElse(throw new PortRangeExhaustedException(
config.localPortMin(),
config.localPortMax()
))
log.info(s"Take next configured free port: $port")
taken += port
port
}
def appPorts(app: AppDefinition) = {
val alreadyAssigned = mutable.Queue(from.app(app.id).map(_.ports).getOrElse(Nil).filter(portRange.contains): _*)
def nextFreeAppPort = if (alreadyAssigned.nonEmpty) alreadyAssigned.dequeue() else nextGlobalFreePort
val ports = app.ports.map { port => if (port == 0) nextFreeAppPort else port }

def assignPorts(app: AppDefinition): AppDefinition = {
val alreadyAssigned = mutable.Queue(
from.app(app.id)
.map(_.ports)
.getOrElse(Nil)
.filter(portRange.contains): _*
)

def nextFreeAppPort: JInt =
if (alreadyAssigned.nonEmpty) alreadyAssigned.dequeue()
else nextGlobalFreePort

// dynamic ports defined in container port mappings
val containerPorts: Option[Seq[JInt]] =
app.containerHostPorts.map(_.map(p => new JInt(p)))

val ports: Seq[JInt] = containerPorts.getOrElse(app.ports)
.map { port => if (port == 0) nextFreeAppPort else port }

app.copy(ports = ports)
}
val dynamicApps = to.transitiveApps.filter(_.hasDynamicPort).map(appPorts)
dynamicApps.foldLeft(to) { (update, app) => update.updateApp(app.id, _ => app, app.version) }

val dynamicApps: Set[AppDefinition] =
to.transitiveApps.filter(_.hasDynamicPort).map(assignPorts)

dynamicApps.foldLeft(to) { (update, app) =>
update.updateApp(app.id, _ => app, app.version)
}
}

}