├── project ├── build.properties ├── paradox.sbt └── plugins.sbt ├── README.md ├── docs ├── build.sbt └── src │ └── main │ └── paradox │ ├── running.md │ ├── locally.md │ ├── kubernetes.md │ ├── index.md │ ├── networking.md │ ├── code-details.md │ └── openshift.md ├── conf ├── selfsigned.keystore ├── routes ├── logback.xml └── application.conf ├── public ├── images │ ├── external.png │ ├── favicon.png │ └── header-pattern.png ├── javascripts │ └── hello.js └── stylesheets │ └── main.css ├── .gitignore ├── deployment ├── base │ ├── kustomization.yaml │ ├── service.yml │ └── deployment.yml └── overlays │ ├── minikube │ ├── ingress.yml │ └── kustomization.yaml │ └── my-openshift-cluster │ ├── kustomization.yaml │ └── route.yml ├── scripts ├── test-sbt └── test-gradle ├── NOTICE ├── app ├── protobuf │ └── helloworld.proto ├── Module.scala ├── routers │ └── HelloWorldRouter.scala └── controllers │ └── HomeController.scala ├── .mergify.yml ├── ssl-play ├── .travis.yml ├── test └── test │ ├── HelloSpecs2Spec.scala │ └── HelloScalaTestSpec.scala ├── .github └── settings.yml └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MOVED TO https://github.com/playframework/play-samples 2 | -------------------------------------------------------------------------------- /project/paradox.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.5") -------------------------------------------------------------------------------- /docs/build.sbt: -------------------------------------------------------------------------------- 1 | paradoxTheme := Some(builtinParadoxTheme("generic")) 2 | 3 | scalaVersion := "2.12.8" 4 | 5 | -------------------------------------------------------------------------------- /conf/selfsigned.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play-scala-grpc-example/2.7.x/conf/selfsigned.keystore -------------------------------------------------------------------------------- /public/images/external.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play-scala-grpc-example/2.7.x/public/images/external.png -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play-scala-grpc-example/2.7.x/public/images/favicon.png -------------------------------------------------------------------------------- /public/javascripts/hello.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log("Welcome to your Play application's JavaScript!"); 3 | } 4 | -------------------------------------------------------------------------------- /public/images/header-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play-scala-grpc-example/2.7.x/public/images/header-pattern.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | build 4 | /.idea 5 | /.idea_modules 6 | /.classpath 7 | /.project 8 | /.settings 9 | /.gradle 10 | /RUNNING_PID 11 | *.iml 12 | -------------------------------------------------------------------------------- /deployment/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | resources: 6 | - deployment.yml 7 | - service.yml 8 | -------------------------------------------------------------------------------- /scripts/test-sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "+----------------------------+" 4 | echo "| Executing tests using sbt |" 5 | echo "+----------------------------+" 6 | ./ssl-play ++$TRAVIS_SCALA_VERSION test docs/paradox 7 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.7.0") 2 | 3 | // #grpc_sbt_plugin 4 | // project/plugins.sbt 5 | addSbtPlugin("com.lightbend.akka.grpc" %% "sbt-akka-grpc" % "0.6.0") 6 | // #grpc_sbt_plugin 7 | -------------------------------------------------------------------------------- /docs/src/main/paradox/running.md: -------------------------------------------------------------------------------- 1 | 2 | ## Running 3 | 4 | * Running on a cluster: refer to the specific guides for @ref:[OpenShift](openshift.md) and @ref:[Kubernetes (`minikube`)](kubernetes.md) 5 | for specific information on deploying in Kubernetes-based clusters. 6 | 7 | * Run @ref[locally](locally.md) 8 | -------------------------------------------------------------------------------- /deployment/overlays/minikube/ingress.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "extensions/v1beta1" 3 | kind: Ingress 4 | metadata: 5 | name: "play-scala-grpc-ingress" 6 | spec: 7 | rules: 8 | - host: "myservice.example.org" 9 | http: 10 | paths: 11 | - backend: 12 | serviceName: "play-scala-grpc-example" 13 | servicePort: 9000 14 | 15 | -------------------------------------------------------------------------------- /deployment/overlays/my-openshift-cluster/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | bases: 6 | - ../../base/ 7 | resources: 8 | - route.yml 9 | 10 | images: 11 | - name: "play-scala-grpc-example" 12 | newName: "my-docker-registry.mycompany.com/play-scala-grpc-example/play-scala-grpc-example" 13 | newTag: "1.0-SNAPSHOT" -------------------------------------------------------------------------------- /deployment/base/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | labels: 6 | appName: "play-scala-grpc-example" 7 | name: "play-scala-grpc-example" 8 | spec: 9 | ports: 10 | - name: http 11 | port: 9000 12 | protocol: TCP 13 | - name: https 14 | port: 9443 15 | protocol: TCP 16 | selector: 17 | appName: "play-scala-grpc-example" 18 | -------------------------------------------------------------------------------- /deployment/overlays/minikube/kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | bases: 6 | - ../../base/ 7 | resources: 8 | - ingress.yml 9 | 10 | ## The minikube kustomization doesn't doe anything because the deployment.yml has all the right defaults. 11 | #images: 12 | #- name: "play-scala-grpc-example" 13 | # newName: "play-scala-grpc-example" 14 | # newTag: "1.0-SNAPSHOT" -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Written by Lightbend 2 | 3 | To the extent possible under law, the author(s) have dedicated all copyright and 4 | related and neighboring rights to this software to the public domain worldwide. 5 | This software is distributed without any warranty. 6 | 7 | You should have received a copy of the CC0 Public Domain Dedication along with 8 | this software. If not, see . 9 | -------------------------------------------------------------------------------- /app/protobuf/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | option java_package = "example.myapp.helloworld.grpc"; 5 | option java_outer_classname = "HelloWorldProto"; 6 | 7 | package helloworld; 8 | 9 | service GreeterService { 10 | rpc SayHello (HelloRequest) returns (HelloReply) {} 11 | } 12 | 13 | message HelloRequest { 14 | string name = 1; 15 | } 16 | 17 | message HelloReply { 18 | string message = 1; 19 | } 20 | -------------------------------------------------------------------------------- /scripts/test-gradle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Using cut because TRAVIS_SCALA_VERSION is the full Scala 4 | # version (for example 2.12.4), but Gradle expects just the 5 | # binary version (for example 2.12) 6 | scala_binary_version=$(echo $TRAVIS_SCALA_VERSION | cut -c1-4) 7 | 8 | echo "+------------------------------+" 9 | echo "| Executing tests using Gradle |" 10 | echo "+------------------------------+" 11 | ./gradlew -Dscala.binary.version=$scala_binary_version check -i --stacktrace 12 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # An example controller showing a sample home page 6 | GET / controllers.HomeController.index 7 | 8 | # ---- gRPC services ---- 9 | -> / routers.HelloWorldRouter 10 | # ---- end of gRPC services ---- 11 | 12 | # Map static resources from the /public folder to the /assets URL path 13 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) 14 | -------------------------------------------------------------------------------- /app/Module.scala: -------------------------------------------------------------------------------- 1 | import com.google.inject.AbstractModule 2 | 3 | /** 4 | * This class is a Guice module that tells Guice how to bind several 5 | * different types. This Guice module is created when the Play 6 | * application starts. 7 | * 8 | * Play will automatically use any class called `Module` that is in 9 | * the root package. You can create modules in other locations by 10 | * adding `play.modules.enabled` settings to the `application.conf` 11 | * configuration file. 12 | */ 13 | class Module extends AbstractModule { 14 | 15 | override def configure(): Unit = { 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /deployment/overlays/my-openshift-cluster/route.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: route.openshift.io/v1 3 | kind: Route 4 | metadata: 5 | labels: 6 | appName: play-scala-grpc-example 7 | name: play-scala-grpc-route 8 | namespace: play-scala-grpc-example 9 | selfLink: >- 10 | /apis/route.openshift.io/v1/namespaces/play-scala-grpc-example/routes/play-scala-grpc-route 11 | spec: 12 | host: myservice.example.org 13 | port: 14 | targetPort: http 15 | to: 16 | kind: Service 17 | name: play-scala-grpc-example 18 | weight: 100 19 | wildcardPolicy: None 20 | status: 21 | ingress: 22 | - conditions: 23 | host: myservice.example.org 24 | routerName: router 25 | -------------------------------------------------------------------------------- /app/routers/HelloWorldRouter.scala: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.Materializer 5 | import example.myapp.helloworld.grpc.AbstractGreeterServiceRouter 6 | import example.myapp.helloworld.grpc.{ HelloReply, HelloRequest } 7 | import javax.inject.Inject 8 | 9 | import scala.concurrent.Future 10 | 11 | class HelloWorldRouter @Inject()(mat: Materializer, system: ActorSystem) 12 | extends AbstractGreeterServiceRouter(mat, system) { 13 | 14 | // We need to inject a Materializer since it is required by the abstract 15 | // router. It can also be used to access the ExecutionContext if you need 16 | // to transform Futures. For example: 17 | // 18 | // private implicit val matExecutionContext = mat.executionContext 19 | // 20 | // But at this example, this is not necessary. 21 | 22 | override def sayHello(in: HelloRequest): Future[HelloReply] = 23 | Future.successful(HelloReply(s"Hello, ${in.name}!")) 24 | } 25 | -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %coloredLevel %logger{15} - %message%n%xException{10} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Merge PRs that are ready 3 | conditions: 4 | - status-success=Travis CI - Pull Request 5 | - status-success=typesafe-cla-validator 6 | - "#approved-reviews-by>=1" 7 | - "#review-requested=0" 8 | - "#changes-requested-reviews-by=0" 9 | - label!=status:block-merge 10 | actions: 11 | merge: 12 | method: squash 13 | strict: smart 14 | 15 | - name: Merge TemplateControl's PRs that are ready 16 | conditions: 17 | - status-success=Travis CI - Pull Request 18 | - "#review-requested=0" 19 | - "#changes-requested-reviews-by=0" 20 | - label!=status:block-merge 21 | - label=status:merge-when-green 22 | - label!=status:block-merge 23 | actions: 24 | merge: 25 | method: squash 26 | strict: smart 27 | 28 | - name: Delete the PR branch after merge 29 | conditions: 30 | - merged 31 | actions: 32 | delete_head_branch: {} 33 | -------------------------------------------------------------------------------- /app/controllers/HomeController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | // #grpc_client_injection 4 | //app/controllers/HomeController.scala 5 | import com.typesafe.config.Config 6 | import example.myapp.helloworld.grpc.{ GreeterServiceClient, HelloReply, HelloRequest } 7 | import javax.inject.Inject 8 | import play.api.mvc._ 9 | 10 | import scala.concurrent.{ ExecutionContext, Future } 11 | 12 | class HomeController @Inject()(greeterServiceClient: GreeterServiceClient, 13 | config: Config 14 | )(implicit ec: ExecutionContext 15 | ) extends InjectedController { 16 | // #grpc_client_injection 17 | 18 | def index = Action.async { 19 | val request = HelloRequest("Caplin") 20 | // create a gRPC request 21 | val reply: Future[HelloReply] = greeterServiceClient.sayHello(request) 22 | // forward the gRPC response back as a plain String on an HTTP response 23 | reply.map(_.message).map(m => Ok(m)) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ssl-play: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Based on https://github.com/playframework/play-scala-tls-example/edit/2.5.x/play but using a minimal collection of 4 | ## settings to demonstrate gRPC. For a larger list of settings check https://github.com/playframework/play-scala-tls-example/edit/2.5.x/play 5 | 6 | # Turn on HTTPS, turn off HTTP. 7 | # This should be https://example.com:9443 8 | JVM_OPTIONS="$JVM_OPTIONS -Dhttp.port=disabled" 9 | JVM_OPTIONS="$JVM_OPTIONS -Dhttps.port=9443" 10 | 11 | # ssl-play requires an ALPN Agent. This is downdloaded and stored in the target folder. 12 | # This snippet detects when the ALPN agent hasn't been downloaded yet and runs 13 | # `sbt stage` to obtain it. 14 | export AGENT_TEST=$(pwd)/$(find target -name 'jetty-alpn-agent-*.jar') 15 | export NUM_AGENTS_FOUND=$(echo $AGENT_TEST| grep target | wc -l) 16 | 17 | if [ $NUM_AGENTS_FOUND -eq "0" ]; then 18 | sbt stage; 19 | fi 20 | 21 | # Start `sbt` with the JVM_OPTIONS and the ALPN agent 22 | export AGENT=$(pwd)/$(find target -name 'jetty-alpn-agent-*.jar' | head -1) 23 | echo "Detected ALPN Agent: $AGENT " 24 | export SBT_OPTS="$SBT_OPTS -javaagent:$AGENT" 25 | # Run Play 26 | sbt $JVM_OPTIONS $*; 27 | 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 2.12.8 3 | script: $SCRIPT 4 | 5 | env: 6 | matrix: 7 | - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.8.202-08 8 | - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-2 9 | - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.202-08 10 | - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-2 11 | 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.202-08 # current gradle doesn't support play 2.7 16 | - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-2 # current gradle doesn't support play 2.7 17 | - env: SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-2 # not fully supported but allows problem discovery 18 | 19 | before_install: curl -Ls https://git.io/jabba | bash && . ~/.jabba/jabba.sh 20 | install: jabba install "$TRAVIS_JDK" && jabba use "$_" && java -Xmx32m -version 21 | 22 | cache: 23 | directories: 24 | - "$HOME/.gradle/caches" 25 | - "$HOME/.ivy2/cache" 26 | - "$HOME/.jabba/jdk" 27 | - "$HOME/.sbt" 28 | 29 | before_cache: 30 | - find $HOME/.ivy2 -name "ivydata-*.properties" -delete 31 | - find $HOME/.sbt -name "*.lock" -delete 32 | -------------------------------------------------------------------------------- /deployment/base/deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "apps/v1beta2" 3 | kind: Deployment 4 | metadata: 5 | name: "play-scala-grpc-example-v1-0-snapshot" 6 | labels: 7 | appName: "play-scala-grpc-example" 8 | appNameVersion: "play-scala-grpc-example-v1-0-snapshot" 9 | spec: 10 | replicas: 2 11 | selector: 12 | matchLabels: 13 | appNameVersion: "play-scala-grpc-example-v1-0-snapshot" 14 | template: 15 | metadata: 16 | labels: 17 | appName: "play-scala-grpc-example" 18 | appNameVersion: "play-scala-grpc-example-v1-0-snapshot" 19 | spec: 20 | restartPolicy: Always 21 | containers: 22 | - name: "play-scala-grpc-example" 23 | image: "play-scala-grpc-example:1.0-SNAPSHOT" 24 | imagePullPolicy: IfNotPresent 25 | env: 26 | - name: "JAVA_OPTS" 27 | value: "-Dplay.http.secret.key=a-very-strong-key-for-production -Dplay.filters.hosts.allowed.0=myservice.example.org -Dplay.server.pidfile.path=/dev/null" 28 | - name: "TRANSPORT_HTTP_BIND_ADDRESS" 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: "status.podIP" 32 | - name: "TRANSPORT_HTTPS_BIND_ADDRESS" 33 | valueFrom: 34 | fieldRef: 35 | fieldPath: "status.podIP" 36 | - name: "DEPLOYMENT_SERVICE_NAME" 37 | value: "play-scala-grpc-example" 38 | ports: 39 | - containerPort: 9000 40 | name: http 41 | - containerPort: 9443 42 | name: https 43 | volumeMounts: [] 44 | command: 45 | - "/opt/docker/bin/play-scala-grpc-example" 46 | volumes: [] -------------------------------------------------------------------------------- /docs/src/main/paradox/locally.md: -------------------------------------------------------------------------------- 1 | # Running Locally 2 | 3 | Running this application requires [sbt](http://www.scala-sbt.org/). gRPC, in turn, requires the transport to be 4 | HTTP/2 so we want Play to use HTTP/2. On top of that, we will also enable HTTPS. These requirements limit which 5 | setups are supported to run Play and only the following can be used at the moment: 6 | 7 | 1. you may use `sbt runProd` to run Play locally in a forked JVM in PROD mode, or 8 | 1. you may use `./ssl-play run` to run Play in DEV mode within `sbt`. 9 | 10 | `./ssl-play` is a wrapper script around `sbt` that sets up the ALPN agent (required for HTTP/2) on the JVM running `sbt`. 11 | 12 | In both execution modes above, `sbt` will also generate the server and client sources based on the `app/protobuf/*.proto` 13 | files. The code generation happens thanks to the Akka gRPC plugin being enabled. See 14 | @ref[understanding the code](code-details.md) for more details. 15 | 16 | Finally, for your convenience, a self-signed certificate for `CN='localhost'` is provided in this 17 | example (see `conf/selfsigned.keystore`). Setting up a keystore works different in DEV mode and PROD mode. Locate 18 | the `play.server.https.keyStore.path` setting in `application.conf` and `build.sbt` for an example on how to set 19 | the keystore on each environment. 20 | s 21 | ## Verifying 22 | 23 | Finally, since now we know what the application is: an HTTP endpoint that hits its own gRPC endpoint to reply to the incoming request. 24 | We can trigger such request and see it correctly reply with a "Hello Caplin!" (which is the name of a nice Capybara, google it): 25 | 26 | ``` 27 | $ curl --insecure https://localhost:9443 ; echo 28 | Hello Caplin! 29 | ``` 30 | -------------------------------------------------------------------------------- /test/test/HelloSpecs2Spec.scala: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import example.myapp.helloworld.grpc.{ GreeterService, GreeterServiceClient, HelloRequest } 4 | import io.grpc.Status 5 | import play.api.inject.bind 6 | import play.api.inject.guice.GuiceApplicationBuilder 7 | import play.api.libs.ws.{ WSClient, WSRequest } 8 | import play.api.routing.Router 9 | import play.api.test._ 10 | import play.grpc.specs2.ServerGrpcClient 11 | import routers.HelloWorldRouter 12 | 13 | class HelloSpecs2Spec extends ForServer with ServerGrpcClient with PlaySpecification with ApplicationFactories { 14 | 15 | protected def applicationFactory: ApplicationFactory = 16 | withGuiceApp(GuiceApplicationBuilder().overrides(bind[Router].to[HelloWorldRouter])) 17 | 18 | def wsUrl(path: String)(implicit running: RunningServer): WSRequest = { 19 | val ws = running.app.injector.instanceOf[WSClient] 20 | val url = running.endpoints.httpEndpoint.get.pathUrl(path) 21 | ws.url(url) 22 | } 23 | 24 | "A Play server bound to a gRPC router" should { 25 | "give a 404 when routing a non-gRPC request" >> { implicit rs: RunningServer => 26 | val result = await(wsUrl("/").get) 27 | result.status must ===(404) 28 | } 29 | "give an Ok header when routing a non-existent gRPC method" >> { implicit rs: RunningServer => 30 | val result = await(wsUrl(s"/${GreeterService.name}/FooBar").get) 31 | result.status must ===(200) 32 | } 33 | "give a 200 when routing an empty request to a gRPC method" >> { implicit rs: RunningServer => 34 | val result = await(wsUrl(s"/${GreeterService.name}/SayHello").get) 35 | result.status must ===(200) 36 | } 37 | "work with a gRPC client" >> { implicit rs: RunningServer => 38 | withGrpcClient[GreeterServiceClient] { client: GreeterServiceClient => 39 | val reply = await(client.sayHello(HelloRequest("Alice"))) 40 | reply.message must ===("Hello, Alice!") 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /test/test/HelloScalaTestSpec.scala: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import example.myapp.helloworld.grpc.{ GreeterService, GreeterServiceClient, HelloRequest } 4 | import org.scalatest.concurrent.{ IntegrationPatience, ScalaFutures } 5 | import org.scalatestplus.play.PlaySpec 6 | import org.scalatestplus.play.guice.GuiceOneServerPerTest 7 | import play.api.Application 8 | import play.api.inject.bind 9 | import play.api.inject.guice.GuiceApplicationBuilder 10 | import play.api.libs.ws.WSClient 11 | import play.api.routing.Router 12 | import play.grpc.scalatest.ServerGrpcClient 13 | import routers.HelloWorldRouter 14 | 15 | class HelloScalaTestSpec extends PlaySpec with GuiceOneServerPerTest with ServerGrpcClient 16 | with ScalaFutures with IntegrationPatience { 17 | 18 | override def fakeApplication(): Application = 19 | GuiceApplicationBuilder().overrides(bind[Router].to[HelloWorldRouter]).build() 20 | 21 | implicit def ws: WSClient = app.injector.instanceOf(classOf[WSClient]) 22 | 23 | "A Play server bound to a gRPC router" must { 24 | "give a 404 when routing a non-gRPC request" in { 25 | val result = wsUrl("/").get.futureValue 26 | result.status must be(404) // Maybe should be a 426, see #396 27 | } 28 | "give an Ok header (and hopefully a not implemented trailer) when routing a non-existent gRPC method" in { 29 | val result = wsUrl(s"/${GreeterService.name}/FooBar").get.futureValue 30 | result.status must be(200) // Maybe should be a 426, see #396 31 | // TODO: Test that trailer has a not implemented status 32 | } 33 | "give a 200 when routing an empty request to a gRPC method" in { 34 | val result = wsUrl(s"/${GreeterService.name}/SayHello").get.futureValue 35 | result.status must be(200) // Maybe should be a 426, see #396 36 | } 37 | "work with a gRPC client" in withGrpcClient[GreeterServiceClient] { client: GreeterServiceClient => 38 | val reply = client.sayHello(HelloRequest("Alice")).futureValue 39 | reply.message must be("Hello, Alice!") 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/src/main/paradox/kubernetes.md: -------------------------------------------------------------------------------- 1 | # Deploy on Kubernetes 2 | 3 | 4 | ### Prerequisites 5 | 6 | Install the following: 7 | 8 | * [Docker](https://docs.docker.com/install/) 9 | * [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 10 | * [Minikube](https://github.com/kubernetes/minikube) 11 | * [`kustomize`](https://github.com/kubernetes-sigs/kustomize) (v2.0.0+) 12 | * [Sbt](https://www.scala-sbt.org/) 13 | 14 | 15 | ### Running 16 | 17 | Once minikube is running the application can be deployed. Create the image: 18 | 19 | ```bash 20 | $ eval $(minikube docker-env) 21 | $ sbt docker:publishLocal 22 | ``` 23 | 24 | Apply the `Deployment`, the `Service` and the `Ingress` into your `minikube` cluster: 25 | 26 | ```bash 27 | $ kustomize build deployment/overlays/minikube | kubectl apply -f - 28 | ``` 29 | 30 | Verify the deployment status: 31 | 32 | ``` 33 | $ kubectl get all 34 | NAME READY STATUS RESTARTS AGE 35 | pod/play-scala-grpc-example-v1-0-snapshot-6c7b575d86-9ql9r 1/1 Running 0 3m 36 | pod/play-scala-grpc-example-v1-0-snapshot-6c7b575d86-jlsfq 1/1 Running 0 3m 37 | 38 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 39 | service/kubernetes ClusterIP 10.96.0.1 443/TCP 17h 40 | service/play-scala-grpc-example ClusterIP 10.106.226.87 9000/TCP,9443/TCP 3m 41 | 42 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 43 | deployment.apps/play-scala-grpc-example-v1-0-snapshot 2 2 2 2 3m 44 | 45 | NAME DESIRED CURRENT READY AGE 46 | replicaset.apps/play-scala-grpc-example-v1-0-snapshot-6c7b575d86 2 2 2 3m 47 | ``` 48 | 49 | And send a request: 50 | 51 | ``` 52 | $ curl -H "Host: myservice.example.org" http://`minikube ip`/ 53 | Hello, Caplin! 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /docs/src/main/paradox/index.md: -------------------------------------------------------------------------------- 1 | # Play Scala gRPC Example 2 | 3 | This example application shows how to use Akka gRPC to both expose and use gRPC services inside an Play application. 4 | 5 | The [Play Framework](https://www.playframework.com/) combines productivity and performance making it easy to build 6 | scalable web applications with Java and Scala. Play is developer friendly with a "just hit refresh" workflow and 7 | built-in testing support. With Play, applications scale predictably due to a stateless and non-blocking architecture. 8 | 9 | [Akka gRPC](https://developer.lightbend.com/docs/akka-grpc/current/overview.html) is a toolkit for building streaming 10 | gRPC servers and clients on top of Akka Streams. 11 | 12 | For detailed documentation refer to https://www.playframework.com/documentation/latest/Home and https://developer.lightbend.com/docs/akka-grpc/current/. 13 | 14 | ## Obtaining this example 15 | 16 | You may download the code from [GitHub](https://github.com/playframework/play-scala-grpc-example) directly or you can 17 | kickstart your Play gRPC project on [Lightbend's Tech Hub](https://developer.lightbend.com/start/?group=play&project=play-scala-grpc-example). 18 | 19 | ## What this example does 20 | 21 | This example runs a Play application which serves both HTTP/1.1 and gRPC (over HTTP/2) enpoints. This application also 22 | uses an Akka-gRPC client to send a request to itself. When you sent a `GET` request `/` the request is handled by a 23 | vanilla Play `Controller` that sends a request over gRPC to the gRPC endpoint: 24 | 25 | 26 | ``` 27 | --------------- 28 | | | 29 | -- (HTTP/1.1) --> > Controller --> --+ 30 | | | | 31 | | | | 32 | +-------> > gRPC Router | | 33 | | | | | 34 | | ---------------- | 35 | | | 36 | +------------ (HTTP/2) -------+ 37 | 38 | ``` 39 | 40 | When deploying this application on Kubernetes or Openshift, there are some extra considerations wrt request rounting. 41 | Refer to @ref:[Networking](networking.md) for more details on how this sample works on production environments. 42 | 43 | ## Running 44 | 45 | * Running on a cluster: refer to the specific guides for @ref:[OpenShift](openshift.md) and @ref:[Kubernetes (`minikube`)](kubernetes.md) 46 | for specific information on deploying in Kubernetes-based clusters. 47 | 48 | * Run @ref[locally](locally.md) 49 | 50 | 51 | ## Understanding the code 52 | 53 | Refer to the @ref[understanding the code](code-details.md) for more details on how this example application works. 54 | 55 | @@@ index 56 | 57 | * [Networking](networking.md) 58 | * [Running](running.md) 59 | * [Running on OpenShift](openshift.md) 60 | * [Running on Kubernetes (`minikube`)](kubernetes.md) 61 | * [Running locally](locally.md) 62 | * [understanding the code](code-details.md) 63 | 64 | @@@ 65 | -------------------------------------------------------------------------------- /docs/src/main/paradox/networking.md: -------------------------------------------------------------------------------- 1 | # Networking 2 | 3 | This sample application serves both HTTP and HTTPS traffic in ports 9000 and 9443 respectively. 4 | When deploying, there are 2 pods behind a Service exposed to the outside via an Ingress/Router. The 5 | Service exposes both 9000 and 9443 but the Ingress/Router only expose the `PLAINTEXT` port. 6 | 7 | The Kubernetes and OpenShift descriptors create an Ingress or Route rules based on the 8 | `myservice.example.org` virtual host. This means that any external request arriving into 9 | the cluster with a `Host: myservice.example.org` header will be forwarded to our 10 | `service/play-scala-grpc-example`. 11 | 12 | 13 | ``` 14 | ----- +---+ 15 | | I | | S | +--------------+ 16 | | N | | E | | | 17 | inet --| G |-- (HTTP/1.1) -->| R |----> Controller ->----+ 18 | | R | | V | | | | 19 | | E | | I | | | | 20 | | S | +--->| C |----> gRPC Router | | 21 | | S | | | E | | | | 22 | ----- | +---+ +--------------+ | 23 | | | 24 | +---------------- (HTTP/2) -------+ 25 | 26 | ``` 27 | 28 | 29 | The code in the `HomeController`, uses a gRPC client to connect to a gRPC Router running on 30 | the same process. The gRPC client is configured to connect to the Service instead of connecting 31 | to the same pod where it running (see the client configuration in `application.conf` using 32 | `DEPLOYMENT_SERVICE_NAME `). 33 | 34 | @@@ note 35 | You can find the deployment descriptors on the `deployment/` folder of this sample application. 36 | @@@ 37 | 38 | ## `use-tls = true` 39 | 40 | This sample demonstrates gRPC over `CYPHERTEXT HTTP/2` so we pay the price of 41 | some added complexity: the Play process is using a self-signed certificate issued to 42 | `localhost`. The consequence of using a certificate issued to `localhost` is that the TLS handshake between the gRPC client 43 | running inside the `HomeController` and the Play server running the gRPC Router will only 44 | succeed if the requests include `Host: localhost` as a header. If the gRPC request was sent to 45 | `Host: my-service-name` the TLS handshake would fail. Therefor we hardcode the `Authority` 46 | to `localhost`. Summing up: the `HomeController` opens a socket to the service public IP 47 | for `HTTP/2 with TLS` but sends a request with the header `Host: localhost` so the TLS handshake 48 | passes the hostname verification. 49 | 50 | #### Using TLS on Kubernetes/OpenShift 51 | 52 | It is out of the scope of this sample application to demonstrate how to use a CA and 53 | a server certificate issued by the Kubernetes/OpenShift Secret manager. Instead, a 54 | previously crafted, self-signed certificate are shipped with the application. 55 | -------------------------------------------------------------------------------- /docs/src/main/paradox/code-details.md: -------------------------------------------------------------------------------- 1 | # Understanding the code 2 | 3 | Adding gRPC support to a vanilla Play application requires a few steps: 4 | 5 | ### 1. `sbt-akka-grpc` 6 | 7 | Add the Akka gRPC plugin on `project/plugins.sbt` 8 | 9 | @@snip [plugins.sbt](../../../../project/plugins.sbt) { #grpc_sbt_plugin } 10 | 11 | and enable it on your project (in `build.sbt`): 12 | 13 | @@snip [build.sbt](../../../../build.sbt) { #grpc_play_plugins } 14 | 15 | The `AkkaGrpcPlugin` locates the gRPC `.proto` files and generates source code from it. Remember to enable the plugin 16 | in all the projects of your build that want to use it. 17 | 18 | Note how the `PlayAkkaHttp2Support` is also enabled. gRPC requires HTTP/2 transport and Play supports it only as an opt-in plugin. 19 | 20 | 21 | ### 2.a Serving (Akka) gRPC Services 22 | 23 | Have a look at the `conf/routes` file where you'll notice how to embed a gRPC router within a normal play application. 24 | You can in fact mix normal Play routes with gRPC routers like this to offer a mixed service. You'll notice that we 25 | bind the `/` path to the `controllers.HomeController` like usual route, 26 | and then we use the `->` router binding syntax to bind the `routers.HelloWorldRouter`. This is because gRPC services 27 | have paths correspond to their "methods", yet this is handled by its internal infrastructure and end-users need 28 | not concern themselves about the exact names – clients too are generated from the appropriate 29 | `app/protobuf/helloworld.proto` file after all. 30 | 31 | You will need to enable the Akka-gRPC generators for server-side code: 32 | 33 | @@snip [build.sbt](../../../../build.sbt) { #grpc_server_generators } 34 | 35 | You can read more about [Service gRPC from a Play App](https://developer.lightbend.com/docs/play-grpc/current/play/serving-grpc.html) in the docs. 36 | 37 | ### 2.b Injecting Akka-gRPC Clients 38 | 39 | Similarily to the server side, the sources are generated by the Akka gRPC plugin by having it configured to emit the client as well: 40 | 41 | @@snip [build.sbt](../../../../build.sbt) { #grpc_client_generators } 42 | 43 | In order to make the gRPC clients easily injectable, we need to enable the following module in Play as well (in this 44 | example app this has been done already though): 45 | 46 | @@snip [application.conf](../../../../conf/application.conf) { #grpc_enable_client_module } 47 | 48 | Which in turn allows us to inject clients to any of the services defined in our `app/proto` directory, just like so: 49 | 50 | @@snip [HomeController.scala](../../../../app/controllers/HomeController.scala) { #grpc_client_injection } 51 | 52 | Since you may want to configure what service discovery or hardcoded location to use for each client, you may do so 53 | as well in `conf/application.conf`, though we will not dive into this here. Refer to the documentation on 54 | [using Akka Discovery for endpoint discovery](https://developer.lightbend.com/docs/akka-grpc/current/client/configuration.html#using-akka-discovery-for-endpoint-discovery) for more details. 55 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | play { 2 | server { 3 | http.address = localhost 4 | http.address = ${?TRANSPORT_HTTP_BIND_ADDRESS} 5 | https.address = localhost 6 | https.address = ${?TRANSPORT_HTTPS_BIND_ADDRESS} 7 | # Ports are hardcoded and the values match the values in the `deployment.yml` kubernetes 8 | # descriptor. We could use ENV_VAR overwrites to make this more flexible too. 9 | http.port = 9000 10 | https.port = 9443 11 | } 12 | } 13 | 14 | ## Configures the keystore to use on production mode. You will probably need to use Env Var 15 | ## overrides (https://github.com/lightbend/config#optional-system-or-env-variable-overrides) 16 | play.server.https.keyStore.path = conf/selfsigned.keystore 17 | 18 | # http://www.playframework.com/documentation/latest/ApplicationSecret 19 | play.http.secret.key = "default-value-used-locally" 20 | 21 | # #grpc_enable_client_module 22 | # conf/application.conf 23 | ## Modules - https://www.playframework.com/documentation/latest/Modules 24 | play.modules { 25 | # To enable Akka gRPC clients to be @Injected 26 | # This Module is generated by the Akka gRPC sbt plugin. See your `target/scala-2.12/src_managed` folder. 27 | enabled += example.myapp.helloworld.grpc.AkkaGrpcClientModule 28 | } 29 | # #grpc_enable_client_module 30 | 31 | # And we can configure the default target where the gRPC services are expected to run: 32 | # (Alternatively Akka service discovery can be used to discover them) 33 | # 34 | # --------------- 35 | # | | 36 | # -- (HTTP/1.1) --> > Controller --> --+ 37 | # | | | 38 | # | | | 39 | # +-------> > gRPC Router | | 40 | # | | | | 41 | # | ---------------- | 42 | # | | 43 | # +------------ (HTTP/2) -------+ 44 | # 45 | # The settings below configure the client that consumes "helloworld.GreeterService". Because a `host` and `port` 46 | # are used, the client will directly point there. The `ssl-config` section of the settings is required because when 47 | # running this example application we use a self-signed certificate. Therefore we need to tell the client to trust 48 | # the invalid certificate. 49 | akka.grpc.client { 50 | 51 | "helloworld.GreeterService" { 52 | # default `host` to the address where the server's HTTPS endpoint was bound but use the `DEPLOYMENT_SERVICE_NAME` 53 | # when available. 54 | host = ${play.server.https.address} 55 | host = ${?DEPLOYMENT_SERVICE_NAME} 56 | port = ${play.server.https.port} 57 | use-tls = true 58 | # The Authority on the requests must match the CN on the server certificate. The certificate on 59 | # `conf/selfsigned.keystore` was issued to `localhost` so we must override the authority. 60 | override-authority = "localhost" 61 | 62 | # configure ssl to trust our fake certificate chain. 63 | ssl-config { 64 | trustManager = { 65 | stores = [ 66 | {type = "JKS", path = ${user.dir}/conf/selfsigned.keystore} 67 | ] 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # These settings are synced to GitHub by https://probot.github.io/apps/settings/ 2 | repository: 3 | homepage: "https://developer.lightbend.com/start/?group=play" 4 | topics: playframework, example, example-project, sample, sample-app, jvm, webapp 5 | private: false 6 | has_issues: true 7 | # We don't need projects in sample projects 8 | has_projects: false 9 | # We don't need wiki in sample projects 10 | has_wiki: false 11 | has_downloads: true 12 | default_branch: 2.7.x 13 | allow_squash_merge: true 14 | allow_merge_commit: false 15 | allow_rebase_merge: false 16 | 17 | teams: 18 | - name: core 19 | permission: admin 20 | - name: integrators 21 | permission: write 22 | - name: write-bots 23 | permission: write 24 | 25 | branches: 26 | - name: "[0-9].*.x" 27 | protection: 28 | # We don't require reviews for sample applications because they are mainly 29 | # updated by template-control, which is an automated process 30 | required_pull_request_reviews: null 31 | # Required. Require status checks to pass before merging. Set to null to disable 32 | required_status_checks: 33 | # Required. The list of status checks to require in order to merge into this branch 34 | contexts: ["Travis CI - Pull Request", "typesafe-cla-validator"] 35 | 36 | # Labels: tailored list of labels to be used by sample applications 37 | labels: 38 | - color: f9d0c4 39 | name: "closed:declined" 40 | - color: f9d0c4 41 | name: "closed:duplicated" 42 | oldname: duplicate 43 | - color: f9d0c4 44 | name: "closed:invalid" 45 | oldname: invalid 46 | - color: f9d0c4 47 | name: "closed:question" 48 | oldname: question 49 | - color: f9d0c4 50 | name: "closed:wontfix" 51 | oldname: wontfix 52 | - color: 7057ff 53 | name: "good first issue" 54 | - color: 7057ff 55 | name: "Hacktoberfest" 56 | - color: 7057ff 57 | name: "help wanted" 58 | - color: cceecc 59 | name: "status:backlog" 60 | oldname: backlog 61 | - color: b60205 62 | name: "status:block-merge" 63 | oldname: block-merge 64 | - color: b60205 65 | name: "status:blocked" 66 | - color: 0e8a16 67 | name: "status:in-progress" 68 | - color: 0e8a16 69 | name: "status:merge-when-green" 70 | oldname: merge-when-green 71 | - color: fbca04 72 | name: "status:needs-backport" 73 | - color: fbca04 74 | name: "status:needs-forwardport" 75 | - color: fbca04 76 | name: "status:needs-info" 77 | - color: fbca04 78 | name: "status:needs-verification" 79 | - color: 0e8a16 80 | name: "status:ready" 81 | - color: fbca04 82 | name: "status:to-review" 83 | oldname: review 84 | - color: c5def5 85 | name: "topic:build/tests" 86 | - color: c5def5 87 | name: "topic:dev-environment" 88 | - color: c5def5 89 | name: "topic:documentation" 90 | - color: c5def5 91 | name: "topic:jdk-next" 92 | - color: b60205 93 | name: "type:defect" 94 | oldname: bug 95 | - color: 0052cc 96 | name: "type:feature" 97 | - color: 0052cc 98 | name: "type:improvement" 99 | oldname: enhancement 100 | - color: 0052cc 101 | name: "type:updates" 102 | - color: bf0d92 103 | name: "type:template-control" 104 | oldname: template-control 105 | -------------------------------------------------------------------------------- /docs/src/main/paradox/openshift.md: -------------------------------------------------------------------------------- 1 | # Deploy on OpenShift 2 | 3 | ### Prerequisites 4 | 5 | Install the following: 6 | 7 | * [Docker](https://docs.docker.com/install/) 8 | * [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 9 | * OpenShift's CLI: [`oc`](https://docs.openshift.com/container-platform/3.10/cli_reference/get_started_cli.html#installing-the-cli) (["Installing the CLI"](https://docs.openshift.com/container-platform/3.10/cli_reference/get_started_cli.html#installing-the-cli)) 10 | * [`kustomize`](https://github.com/kubernetes-sigs/kustomize) (v2.0.0+) 11 | * [Sbt](https://www.scala-sbt.org/) 12 | 13 | 14 | #### Preface 15 | 16 | There are [multiple flavors](https://www.openshift.com/products?extIdCarryOver=true&sc_cid=701f2000001OH7iAAG) of `oc` and OpenShift. This guide was tested with: 17 | 18 | ``` 19 | $ oc version 20 | 21 | oc v3.10.45 22 | kubernetes v1.10.0+b81c8f8 23 | features: Basic-Auth 24 | 25 | Server https://mycluster.mycompany.com:443 26 | openshift v3.10.45 27 | kubernetes v1.10.0+b81c8f8 28 | ``` 29 | 30 | This guide uses `mycluster.mycompany.com` as an example, you will have to use your own OpenShift cluster and your 31 | docker image registry or a local `minishift` instance. 32 | 33 | ### Running 34 | 35 | First, let's prepare a few environment variables to make things easier: 36 | 37 | ``` 38 | ## obtain the token at the Console UI on you Openshift server 39 | export TOKEN= 40 | export OPENSHIFT_SERVER=mycluster.mycompany.com 41 | 42 | ## Use a project name that will not clash with other deployments on the cluster 43 | export OPENSHIFT_PROJECT=play-scala-grpc-example 44 | export IMAGE=play-scala-grpc-example 45 | export TAG=1.0-SNAPSHOT 46 | 47 | ## The registry should be accessible from the cluster where you deploy 48 | export DOCKER_REGISTRY_SERVER=my-docker-registry.mycompany.com 49 | export DOCKER_REGISTRY=$DOCKER_REGISTRY_SERVER/$OPENSHIFT_PROJECT 50 | ``` 51 | 52 | Login to OpenShift from your terminal and create the OpenShift project: 53 | 54 | ```bash 55 | oc login https://$OPENSHIFT_SERVER --token=$TOKEN 56 | oc new-project $OPENSHIFT_PROJECT 57 | ``` 58 | 59 | Create the docker image of your application and push it to the image registry. 60 | 61 | ```bash 62 | sbt docker:publishLocal 63 | 64 | docker login -p $TOKEN -u unused $DOCKER_REGISTRY_SERVER 65 | docker tag $IMAGE:$TAG $DOCKER_REGISTRY/$IMAGE:$TAG 66 | docker push $DOCKER_REGISTRY/$IMAGE:$TAG 67 | 68 | ## The `kustomize` step uses a `kustomization.yml` prepared for $DOCKER_REGISTRY/$IMAGE:$TAG. 69 | ## You will have to create your own `deployment/overlays` folder (make a copy of 70 | ## `deployment/overlays/my-openshift-cluster` and edit `kustomization.yml`). 71 | kustomize build deployment/overlays/my-openshift-cluster | oc apply -f - 72 | ``` 73 | 74 | Finally, verify the deployment completed successfully: 75 | 76 | ```bash 77 | $ oc get all 78 | NAME READY STATUS RESTARTS AGE 79 | pod/play-scala-grpc-example-v1-0-snapshot-5b77bd9849-69wws 1/1 Running 0 16h 80 | pod/play-scala-grpc-example-v1-0-snapshot-5b77bd9849-9p657 1/1 Running 0 16h 81 | 82 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 83 | service/play-scala-grpc-example ClusterIP 172.30.205.57 9000/TCP,9443/TCP 17h 84 | 85 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 86 | deployment.apps/play-scala-grpc-example-v1-0-snapshot 2 2 2 2 17h 87 | 88 | NAME DESIRED CURRENT READY AGE 89 | replicaset.apps/play-scala-grpc-example-v1-0-snapshot-5b77bd9849 2 2 2 16h 90 | 91 | NAME DOCKER REPO TAGS UPDATED 92 | imagestream.image.openshift.io/play-scala-grpc-example docker-registry-default.mycluster.mycompany.com/play-scala-grpc-example/play-scala-grpc-example 1.0-SNAPSHOT 17 hours ago 93 | 94 | NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD 95 | route.route.openshift.io/play-scala-grpc-route myservice.example.org play-scala-grpc-example http None 96 | ``` 97 | 98 | Test the application: 99 | 100 | ```bash 101 | $ curl -H "Host: myservice.example.org" \ 102 | http://$OPENSHIFT_PROJECT.$OPENSHIFT_SERVER 103 | Hello, Caplin! 104 | ``` 105 | 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009-2017 Lightbend Inc. 3 | */ 4 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;} 5 | table{border-collapse:collapse;border-spacing:0;} 6 | caption,th,td{text-align:left;font-weight:normal;} 7 | form legend{display:none;} 8 | blockquote:before,blockquote:after,q:before,q:after{content:"";} 9 | blockquote,q{quotes:"" "";} 10 | ol,ul{list-style:none;} 11 | hr{display:none;visibility:hidden;} 12 | :focus{outline:0;} 13 | article{}article h1,article h2,article h3,article h4,article h5,article h6{color:#333;font-weight:bold;line-height:1.25;margin-top:1.3em;} 14 | article h1 a,article h2 a,article h3 a,article h4 a,article h5 a,article h6 a{font-weight:inherit;color:#333;}article h1 a:hover,article h2 a:hover,article h3 a:hover,article h4 a:hover,article h5 a:hover,article h6 a:hover{color:#333;} 15 | article h1{font-size:36px;margin:0 0 18px;border-bottom:4px solid #eee;} 16 | article h2{font-size:25px;margin-bottom:9px;border-bottom:2px solid #eee;} 17 | article h3{font-size:18px;margin-bottom:9px;} 18 | article h4{font-size:15px;margin-bottom:3px;} 19 | article h5{font-size:12px;font-weight:normal;margin-bottom:3px;} 20 | article .subheader{color:#777;font-weight:300;margin-bottom:24px;} 21 | article p{line-height:1.3em;margin:1em 0;} 22 | article p img{margin:0;} 23 | article p.lead{font-size:18px;font-size:1.8rem;line-height:1.5;} 24 | article li>p:first-child{margin-top:0;} 25 | article li>p:last-child{margin-bottom:0;} 26 | article ul li,article ol li{position:relative;padding:4px 0 4px 14px;}article ul li ol,article ol li ol,article ul li ul,article ol li ul{margin-left:20px;} 27 | article ul li:before,article ol li:before{position:absolute;top:8px;left:0;content:"►";color:#ccc;font-size:10px;margin-right:5px;} 28 | article>ol{counter-reset:section;}article>ol li:before{color:#ccc;font-size:13px;} 29 | article>ol>li{padding:6px 0 4px 20px;counter-reset:chapter;}article>ol>li:before{content:counter(section) ".";counter-increment:section;} 30 | article>ol>li>ol>li{padding:6px 0 4px 30px;counter-reset:item;}article>ol>li>ol>li:before{content:counter(section) "." counter(chapter);counter-increment:chapter;} 31 | article>ol>li>ol>li>ol>li{padding:6px 0 4px 40px;}article>ol>li>ol>li>ol>li:before{content:counter(section) "." counter(chapter) "." counter(item);counter-increment:item;} 32 | article em,article i{font-style:italic;line-height:inherit;} 33 | article strong,article b{font-weight:bold;line-height:inherit;} 34 | article small{font-size:60%;line-height:inherit;} 35 | article h1 small,article h2 small,article h3 small,article h4 small,article h5 small{color:#777;} 36 | article hr{border:solid #ddd;border-width:1px 0 0;clear:both;margin:12px 0 18px;height:0;} 37 | article abbr,article acronym{text-transform:uppercase;font-size:90%;color:#222;border-bottom:1px solid #ddd;cursor:help;} 38 | article abbr{text-transform:none;} 39 | article img{max-width:100%;} 40 | article pre{margin:10px 0;border:1px solid #ddd;padding:10px;background:#fafafa;color:#666;overflow:auto;border-radius:5px;} 41 | article code{background:#fafafa;color:#666;font-family:inconsolata, monospace;border:1px solid #ddd;border-radius:3px;height:4px;padding:0;} 42 | article a code{color:#80c846;}article a code:hover{color:#6dae38;} 43 | article pre code{border:0;background:inherit;border-radius:0;line-height:inherit;font-size:14px;} 44 | article pre.prettyprint{border:1px solid #ddd;padding:10px;} 45 | article blockquote,article blockquote p,article p.note{line-height:20px;color:#4c4742;} 46 | article blockquote,article .note{margin:0 0 18px;padding:1px 20px;background:#fff7d6;}article blockquote li:before,article .note li:before{color:#e0bc6f;} 47 | article blockquote code,article .note code{background:#f5d899;border:none;color:inherit;} 48 | article blockquote a,article .note a{color:#6dae38;} 49 | article blockquote pre,article .note pre{background:#F5D899 !important;color:#48484C !important;border:none !important;} 50 | article p.note{padding:15px 20px;} 51 | article table{width:100%;}article table td{padding:8px;} 52 | article table tr{background:#F4F4F7;border-bottom:1px solid #eee;} 53 | article table tr:nth-of-type(odd){background:#fafafa;} 54 | article dl dt{font-weight:bold;} 55 | article dl.tabbed{position:relative;} 56 | article dl.tabbed dt{float:left;margin:0 5px 0 0;border:1px solid #ddd;padding:0 20px;line-height:2;border-radius: 5px 5px 0 0;} 57 | article dl.tabbed dt a{display:block;height:30px;color:#333;text-decoration:none;} 58 | article dl.tabbed dt.current{background: #f7f7f7;} 59 | article dl.tabbed dd{position:absolute;width:100%;left:0;top:30px;} 60 | article dl.tabbed dd pre{margin-top:0;border-top-left-radius:0;} 61 | a{color:#80c846;}a:hover{color:#6dae38;} 62 | p{margin:1em 0;} 63 | h1{-webkit-font-smoothing:antialiased;} 64 | h2{font-weight:bold;font-size:28px;} 65 | hr{clear:both;margin:20px 0 25px 0;border:none;border-top:1px solid #444;visibility:visible;display:block;} 66 | section{padding:50px 0;} 67 | body{background:#f5f5f5;background:#fff;color:#555;font:15px "Helvetica Nueue",sans-serif;padding:0px 0 0px;} 68 | .wrapper{width:960px;margin:0 auto;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;padding:60px 0;}.wrapper:after{content:" ";display:block;clear:both;} 69 | .wrapper article{min-height:310px;width:650px;float:left;} 70 | .wrapper aside{width:270px;float:right;}.wrapper aside ul{margin:2px 0 30px;}.wrapper aside ul a{display:block;padding:3px 0 3px 10px;margin:2px 0;border-left:4px solid #eee;}.wrapper aside ul a:hover{border-color:#80c846;} 71 | .wrapper aside h3{font-size:18px;color:#333;font-weight:bold;line-height:2em;margin:9px 0;border-bottom:1px solid #eee;} 72 | .wrapper aside.stick{position:fixed;right:50%;margin-right:-480px;top:120px;bottom:0;overflow:hidden;} 73 | .half{width:50%;float:left;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;} 74 | header{position:fixed;top:0;z-index:1000;width:100%;height:50px;line-height:50px;padding:30px 0;background:#fff;background:rgba(255, 255, 255, 0.95);border-bottom:1px solid #ccc;box-shadow:0 4px 0 rgba(0, 0, 0, 0.1);}header #logo{position:absolute;left:50%;margin-left:-480px;} 75 | header nav{position:absolute;right:50%;margin-right:-480px;}header nav a{padding:0 10px 4px;font-size:21px;font-weight:500;text-decoration:none;} 76 | header nav a.selected{border-bottom:3px solid #E9E9E9;} 77 | header nav a.download{position:relative;background:#80c846;color:white;margin-left:10px;padding:5px 10px 2px;font-weight:700;border-radius:5px;box-shadow:0 3px 0 #6dae38;text-shadow:-1px -1px 0 rgba(0, 0, 0, 0.2);-webkit-transition:all 70ms ease-out;border:0;}header nav a.download:hover{box-shadow:0 3px 0 #6dae38,0 3px 4px rgba(0, 0, 0, 0.3);} 78 | header nav a.download:active{box-shadow:0 1px 0 #6dae38;top:2px;-webkit-transition:none;} 79 | #download,#getLogo{display:none;position:absolute;padding:5px 20px;width:200px;background:#000;background:rgba(0, 0, 0, 0.8);border-radius:5px;color:#999;line-height:15px;}#download a,#getLogo a{color:#ccc;text-decoration:none;}#download a:hover,#getLogo a:hover{color:#fff;} 80 | #getLogo{text-align:center;}#getLogo h3{font-size:16px;color:#80c846;margin:0 0 15px;} 81 | #getLogo figure{border-radius:3px;margin:5px 0;padding:5px;background:#fff;line-height:25px;width:80px;display:inline-block;}#getLogo figure a{color:#999;text-decoration:none;}#getLogo figure a:hover{color:#666;} 82 | #download{top:85px;right:50%;margin-right:-480px;}#download .button{font-size:16px;color:#80c846;} 83 | #getLogo{top:85px;left:50%;padding:20px;margin-left:-480px;}#getLogo ul{margin:5px 0;} 84 | #getLogo li{margin:1px 0;} 85 | #news{background:#f5f5f5;color:#999;font-size:17px;box-shadow:0 1px 0 rgba(0, 0, 0, 0.1);position:relative;z-index:2;padding:3px 0;}#news ul{box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;background:url(/assets/images/news.png) 10px center no-repeat;padding:19px 0 19px 60px;} 86 | #content{padding:30px 0;} 87 | #top{background:#80c846 url(/assets/images/header-pattern.png) fixed;box-shadow:0 -4px 0 rgba(0, 0, 0, 0.1) inset;padding:0;position:relative;}#top .wrapper{padding:30px 0;} 88 | #top h1{float:left;color:#fff;font-size:35px;line-height:48px;text-shadow:2px 2px 0 rgba(0, 0, 0, 0.1);}#top h1 a{text-decoration:none;color:#fff;} 89 | #top nav{float:right;margin-top:10px;line-height:25px;}#top nav .versions,#top nav form{float:left;margin:0 5px;} 90 | #top nav .versions{height:25px;display:inline-block;border:1px solid #6dae38;border-radius:3px;background:#80c846;background:-moz-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #80c846), color-stop(100%, #6dae38));background:-webkit-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-o-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-ms-linear-gradient(top, #80c846 0%, #6dae38 100%);background:linear-gradient(top, #80c846 0%, #6dae38 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#80c846', endColorstr='#6dae38',GradientType=0 );box-shadow:inset 0 -1px 1px #80c846;text-align:center;color:#fff;text-shadow:-1px -1px 0 #6dae38;}#top nav .versions span{padding:0 4px;position:absolute;}#top nav .versions span:before{content:"⬍";color:rgba(0, 0, 0, 0.4);text-shadow:1px 1px 0 #80c846;margin-right:4px;} 91 | #top nav .versions select{opacity:0;position:relative;z-index:9;} 92 | #top .follow{display:inline-block;border:1px solid #6dae38;border-radius:3px;background:#80c846;background:-moz-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #80c846), color-stop(100%, #6dae38));background:-webkit-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-o-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-ms-linear-gradient(top, #80c846 0%, #6dae38 100%);background:linear-gradient(top, #80c846 0%, #6dae38 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#80c846', endColorstr='#6dae38',GradientType=0 );box-shadow:inset 0 -1px 1px #80c846;text-align:center;vertical-align:middle;color:#fff;text-shadow:-1px -1px 0 #6dae38;padding:4px 8px;text-decoration:none;position:absolute;top:41px;left:50%;margin-left:210px;width:250px;}#top .follow:before{vertical-align:middle;content:url(/assets/images/twitter.png);margin-right:10px;} 93 | #top input{width:80px;-webkit-transition:width 200ms ease-in-out;-moz-transition:width 200ms ease-in-out;}#top input:focus{width:200px;} 94 | #title{width:500px;float:left;font-size:17px;color:#2d6201;} 95 | #quicklinks{width:350px;margin:-15px 0 0 0;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;float:right;padding:30px;background:#fff;color:#888;box-shadow:0 3px 5px rgba(0, 0, 0, 0.2);}#quicklinks h2{color:#80c846;font-size:20px;margin-top:15px;padding:10px 0 5px 0;border-top:1px solid #eee;}#quicklinks h2:first-child{margin:0;padding:0 0 5px 0;border:0;} 96 | #quicklinks p{margin:0;} 97 | #quicklinks a{color:#444;}#quicklinks a:hover{color:#222;} 98 | .tweet{border-bottom:1px solid #eee;padding:6px 0 20px 60px;position:relative;min-height:50px;margin-bottom:20px;}.tweet img{position:absolute;left:0;top:8px;} 99 | .tweet strong{font-size:14px;font-weight:bold;} 100 | .tweet span{font-size:12px;color:#888;} 101 | .tweet p{padding:0;margin:5px 0 0 0;} 102 | footer{padding:40px 0;background:#363736;background:#eee;border-top:1px solid #e5e5e5;color:#aaa;position:relative;}footer .logo{position:absolute;top:55px;left:50%;margin-left:-480px;-webkit-transform:translate3d(0, 0, 0);-moz-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);} 103 | footer:after{content:" ";display:block;clear:both;} 104 | footer .links{width:960px;margin:0 auto;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;margin:0 auto;padding-left:200px;}footer .links:after{content:" ";display:block;clear:both;} 105 | footer .links dl{width:33%;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;padding:0 10px;float:left;} 106 | footer .links dt{color:#80c846;font-weight:bold;} 107 | footer .links a{color:#aaa;text-decoration:none;}footer .links a:hover{color:#888;} 108 | footer .licence{width:960px;margin:0 auto;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;margin:20px auto 0;padding-top:20px;border-top:2px solid #ddd;font-size:12px;}footer .licence:after{content:" ";display:block;clear:both;} 109 | footer .licence .typesafe,footer .licence .zenexity{float:right;} 110 | footer .licence .typesafe{position:relative;top:-3px;margin-left:10px;} 111 | footer .licence a{color:#999;} 112 | div.coreteam{position:relative;min-height:80px;border-bottom:1px solid #eee;}div.coreteam img{width:50px;position:absolute;left:0;top:0;padding:2px;border:1px solid #ddd;} 113 | div.coreteam a{color:inherit;text-decoration:none;} 114 | div.coreteam h2{padding-left:70px;border:none;font-size:20px;} 115 | div.coreteam p{margin-top:5px;padding-left:70px;} 116 | ul.contributors{padding:0;margin:0;list-style:none;}ul.contributors li{padding:6px 0 !important;margin:0;}ul.contributors li:before{content:' ';} 117 | ul.contributors img{width:25px;padding:1px;border:1px solid #ddd;margin-right:5px;vertical-align:middle;} 118 | ul.contributors a{color:inherit;text-decoration:none;} 119 | ul.contributors span{font-weight:bold;color:#666;} 120 | ul.contributors.others li{display:inline-block;width:32.3333%;} 121 | div.list{float:left;width:33.3333%;margin-bottom:30px;} 122 | h2{clear:both;} 123 | span.by{font-size:14px;font-weight:normal;} 124 | form dl{padding:10px 0;} 125 | dd.info{color:#888;font-size:12px;} 126 | dd.error{color:#c00;} 127 | aside a[href^="http"]:after,.doc a[href^="http"]:after{content:url(/assets/images/external.png);vertical-align:middle;margin-left:5px;} 128 | --------------------------------------------------------------------------------