Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0442437

Browse files
committed
Merge pull request linkerd#88 from BuoyantIO/siggy/marathon-namer
introduce marathon namer
2 parents ea1f13c + f7d2b6b commit 0442437

File tree

10 files changed

+659
-9
lines changed

10 files changed

+659
-9
lines changed

docs/config.md

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ of the following parameters:
311311
* *io.l5d.serversets*: [ZooKeeper ServerSets service discovery](#zookeeper)
312312
* *io.l5d.experimental.consul*: [Consul service discovery](#consul) (**experimental**)
313313
* *io.l5d.experimental.k8s*: [Kubernetes service discovery](#disco-k8s) (**experimental**)
314+
* *io.l5d.experimental.marathon*: [Marathon service discovery](#marathon) (**experimental**)
314315
* *prefix* -- This namer will resolve names beginning with this prefix. See
315316
[Configuring routing](#configuring-routing) for more on names. Some namers may
316317
configure a default prefix; see the specific namer section for details.
@@ -422,8 +423,8 @@ For example:
422423
```yaml
423424
namers:
424425
- kind: io.l5d.experimental.consul
425-
- host: 127.0.0.1
426-
port: 2181
426+
host: 127.0.0.1
427+
port: 2181
427428
```
428429

429430
The default _prefix_ is `io.l5d.consul`. (Note that this is *different* from
@@ -457,10 +458,10 @@ For example:
457458
```yaml
458459
namers:
459460
- kind: io.l5d.experimental.k8s
460-
- host: kubernetes.default.cluster.local
461-
port: 443
462-
tls: true
463-
authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
461+
host: kubernetes.default.cluster.local
462+
port: 443
463+
tls: true
464+
authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
464465
```
465466

466467
The default _prefix_ is `io.l5d.k8s`. (Note that this is *different* from
@@ -480,6 +481,46 @@ baseDtab: |
480481
/http/1.1/GET => /io.l5d.k8s/prod/http;
481482
```
482483
484+
<a name="marathon"></a>
485+
### Marathon service discovery (experimental)
486+
487+
linkerd provides support for service discovery via
488+
[Marathon](https://mesosphere.github.io/marathon/). Note that this support is still considered
489+
experimental.
490+
491+
The Marathon namer is configured with kind `io.l5d.experimental.marathon`, and these parameters:
492+
493+
* *host* -- the Marathon master host. (default: marathon.mesos)
494+
* *port* -- the Marathon master port. (default: 80)
495+
* *uriPrefix* -- the Marathon API prefix. (default: empty string). This prefix
496+
depends on your Marathon configuration. For example, running Marathon
497+
locally, the API is avaiable at `localhost:8080/v2/`, while the default setup
498+
on AWS/DCOS is `$(dcos config show core.dcos_url)/marathon/v2/apps`.
499+
500+
For example:
501+
```yaml
502+
namers:
503+
- kind: io.l5d.experimental.marathon
504+
prefix: /io.l5d.marathon
505+
host: marathon.mesos
506+
port: 80
507+
uriPrefix: /marathon
508+
```
509+
510+
The default _prefix_ is `io.l5d.marathon`. (Note that this is *different* from
511+
the name in the configuration block.)
512+
513+
The Marathon namer takes one path component: `app-id`:
514+
515+
* app-id: the id of the marathon application.
516+
517+
Once configured, to use the Marathon namer, you must reference it in
518+
the dtab.
519+
```
520+
baseDtab: |
521+
/http/1.1/GET => /io.l5d.marathon/http;
522+
```
523+
483524
<a name="configuring-routing"></a>
484525
## Configuring routing
485526

examples/marathon.l5d

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namers:
2+
- kind: io.l5d.experimental.marathon
3+
prefix: /io.l5d.marathon
4+
host: marathon.mesos
5+
port: 80
6+
uriPrefix: /marathon
7+
8+
routers:
9+
- protocol: http
10+
baseDtab: |
11+
/host => /io.l5d.marathon;
12+
/method => /$/io.buoyant.http.anyMethodPfx/host;
13+
/http/1.1 => /method;
14+
httpUriInDst: true
15+
servers:
16+
- port: 4140
17+
ip: 0.0.0.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.l5d.experimental.marathon
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.l5d.experimental
2+
3+
import com.fasterxml.jackson.core.JsonParser
4+
import com.twitter.conversions.time._
5+
import com.twitter.finagle.param.Label
6+
import com.twitter.finagle.{Http, Stack, Path}
7+
import io.buoyant.linkerd.{NamerInitializer, Parsing}
8+
import io.buoyant.marathon.v2.{Api, AppIdNamer}
9+
10+
/**
11+
* Supports namer configurations in the form:
12+
*
13+
* <pre>
14+
* namers:
15+
* - kind: io.l5d.experimental.marathon
16+
* prefix: /io.l5d.marathon
17+
* host: marathon.mesos
18+
* port: 80
19+
* uriPrefix: /marathon
20+
* </pre>
21+
*/
22+
object marathon {
23+
24+
/** The mesos master host; default: marathon.mesos */
25+
case class Host(host: String)
26+
implicit object Host extends Stack.Param[Host] {
27+
val default = Host("marathon.mesos")
28+
val parser = Parsing.Param.Text("host")(Host(_))
29+
}
30+
31+
/** The mesos master port; default: 80 */
32+
case class Port(port: Int)
33+
implicit object Port extends Stack.Param[Port] {
34+
val default = Port(80)
35+
val parser = Parsing.Param.Int("port")(Port(_))
36+
}
37+
38+
/** URI Prefix; default: empty string */
39+
case class UriPrefix(uriPrefix: String)
40+
implicit object UriPrefix extends Stack.Param[UriPrefix] {
41+
val default = UriPrefix("")
42+
val parser = Parsing.Param.Text("uriPrefix")(UriPrefix(_))
43+
}
44+
45+
val parser = Parsing.Params(
46+
Host.parser,
47+
Port.parser,
48+
UriPrefix.parser
49+
)
50+
51+
val defaultParams = Stack.Params.empty +
52+
NamerInitializer.Prefix(Path.Utf8("io.l5d.marathon"))
53+
}
54+
55+
/**
56+
* Configures a Marathon namer.
57+
*/
58+
class marathon(val params: Stack.Params) extends NamerInitializer {
59+
def this() = this(marathon.defaultParams)
60+
def withParams(ps: Stack.Params) = new marathon(ps)
61+
62+
def paramKeys = marathon.parser.keys
63+
def readParam(k: String, p: JsonParser) =
64+
withParams(marathon.parser.read(k, p, params))
65+
66+
/**
67+
* Build a Namer backed by Marathon.
68+
*/
69+
def newNamer() = {
70+
val marathon.Host(host) = params[marathon.Host]
71+
val marathon.Port(port) = params[marathon.Port]
72+
val marathon.UriPrefix(uriPrefix) = params[marathon.UriPrefix]
73+
val NamerInitializer.Prefix(path) = params[NamerInitializer.Prefix]
74+
val service = Http.client
75+
.configured(Label("namer" + path.show))
76+
.newService(s"/$$/inet/$host/$port")
77+
78+
new AppIdNamer(Api(service, host, uriPrefix), prefix, 250.millis)
79+
}
80+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.l5d.experimental
2+
3+
import org.scalatest.FunSuite
4+
import scala.io.Source
5+
6+
class MarathonTest extends FunSuite {
7+
8+
test("sanity") {
9+
// ensure it doesn't totally blowup
10+
new marathon().newNamer()
11+
}
12+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package io.buoyant.marathon.v2
2+
3+
import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper}
4+
import com.fasterxml.jackson.module.scala.DefaultScalaModule
5+
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
6+
import com.twitter.finagle.{Service, http}
7+
import com.twitter.logging.Logger
8+
import com.twitter.io.Buf
9+
import com.twitter.util.{Await, Closable, Future, Time, Try}
10+
import java.net.{InetSocketAddress, SocketAddress}
11+
12+
/**
13+
* A partial implementation of the Marathon V2 API:
14+
* https://mesosphere.github.io/marathon/docs/generated/api.html#v2_apps
15+
*/
16+
17+
trait Api {
18+
def getAppIds(): Future[Api.AppIds]
19+
def getAddrs(app: String): Future[Set[SocketAddress]]
20+
}
21+
22+
object Api {
23+
24+
type AppIds = Set[String]
25+
type Client = Service[http.Request, http.Response]
26+
27+
val versionString = "v2"
28+
29+
def apply(c: Client, host: String, uriPrefix: String): Api =
30+
new AppIdApi(c, host, s"$uriPrefix/$versionString")
31+
32+
private[v2] val log = Logger.get("marathon")
33+
34+
private[v2] def mkreq(
35+
path: String,
36+
host: String
37+
): http.Request = {
38+
val req = http.Request(path)
39+
req.method = http.Method.Get
40+
req.host = host
41+
req
42+
}
43+
44+
private[v2] def rspToApps(
45+
rsp: http.Response
46+
): Future[Api.AppIds] =
47+
rsp.status match {
48+
case http.Status.Ok =>
49+
val apps = readJson[AppsRsp](rsp.content).map(_.toApps)
50+
Future.const(apps)
51+
case _ =>
52+
Future.exception(UnexpectedResponse(rsp))
53+
}
54+
55+
private[v2] def rspToAddrs(
56+
rsp: http.Response
57+
): Future[Set[SocketAddress]] =
58+
rsp.status match {
59+
case http.Status.Ok =>
60+
val addrs = readJson[AppRsp](rsp.content).map(_.toAddresses)
61+
Future.const(addrs)
62+
case _ =>
63+
Future.exception(UnexpectedResponse(rsp))
64+
}
65+
66+
private[this] case class UnexpectedResponse(rsp: http.Response) extends Throwable
67+
68+
private[this] val mapper = new ObjectMapper with ScalaObjectMapper
69+
mapper.registerModule(DefaultScalaModule)
70+
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
71+
private[this] def readJson[T: Manifest](buf: Buf): Try[T] = {
72+
val Buf.ByteArray.Owned(bytes, begin, end) = Buf.ByteArray.coerce(buf)
73+
Try(mapper.readValue[T](bytes, begin, end - begin))
74+
}
75+
76+
private[this] case class TaskNode(
77+
id: Option[String],
78+
host: Option[String],
79+
ports: Option[Seq[Int]]
80+
)
81+
82+
private[this] case class AppNode(
83+
id: Option[String],
84+
tasks: Option[Seq[TaskNode]]
85+
)
86+
87+
private[this] case class AppsRsp(
88+
apps: Option[Seq[AppNode]] = None
89+
) {
90+
def toApps: Api.AppIds =
91+
apps match {
92+
case Some(apps) => apps.map { app => app.id.getOrElse("") }.toSet
93+
case None => Set.empty[String]
94+
}
95+
}
96+
97+
private[this] case class AppRsp(
98+
app: Option[AppNode] = None
99+
) {
100+
def toAddresses: Set[SocketAddress] =
101+
app match {
102+
case Some(AppNode(_, Some(tasks))) =>
103+
tasks.collect {
104+
case TaskNode(_, Some(host), Some(Seq(port, _*))) =>
105+
new InetSocketAddress(host, port)
106+
}.toSet[SocketAddress]
107+
case _ => Set.empty[SocketAddress]
108+
}
109+
}
110+
}
111+
112+
private[this] class AppIdApi(client: Api.Client, host: String, apiPrefix: String) extends Closable
113+
with Api {
114+
115+
import Api._
116+
117+
def close(deadline: Time) = client.close(deadline)
118+
119+
def getAppIds(): Future[Api.AppIds] = {
120+
val req = mkreq(s"$apiPrefix/apps", host)
121+
client(req).flatMap(rspToApps(_))
122+
}
123+
124+
def getAddrs(app: String): Future[Set[SocketAddress]] = {
125+
val req = mkreq(s"$apiPrefix/apps/$app", host)
126+
client(req).flatMap(rspToAddrs(_))
127+
}
128+
}

0 commit comments

Comments
 (0)