├── .gitignore ├── .sbtopts ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── RELEASING.md ├── build-mac-osx ├── build-windows.ps1 ├── build.sbt ├── cli ├── build.sbt ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── com │ │ └── lightbend │ │ └── rp │ │ └── reactivecli │ │ └── Platform.scala ├── native │ └── src │ │ ├── main │ │ └── scala │ │ │ └── com │ │ │ └── lightbend │ │ │ └── rp │ │ │ └── reactivecli │ │ │ ├── Platform.scala │ │ │ ├── http │ │ │ ├── NativeHttp.scala │ │ │ └── nativebinding │ │ │ │ ├── libcurl.scala │ │ │ │ └── nativebinding.scala │ │ │ └── process │ │ │ └── NativeProcess.scala │ │ └── test │ │ └── scala │ │ └── com │ │ └── lightbend │ │ └── rp │ │ └── reactivecli │ │ └── http │ │ ├── NativeHttpTest.scala │ │ └── PlatformTest.scala └── shared │ └── src │ ├── main │ └── scala │ │ └── com │ │ └── lightbend │ │ └── rp │ │ └── reactivecli │ │ ├── Main.scala │ │ ├── annotations │ │ ├── Annotation.scala │ │ ├── Annotations.scala │ │ ├── Endpoint.scala │ │ ├── EnvironmentVariable.scala │ │ ├── Ingress.scala │ │ ├── Module.scala │ │ ├── Secret.scala │ │ ├── kubernetes │ │ │ ├── ConfigMapEnvironmentVariable.scala │ │ │ ├── FieldRefEnvironmentVariable.scala │ │ │ └── SecretKeyRefEnvironmentVariable.scala │ │ └── package.scala │ │ ├── argonaut │ │ └── YamlRenderer.scala │ │ ├── argparse │ │ ├── CommandArgs.scala │ │ ├── InputArgs.scala │ │ ├── TargetRuntimeArgs.scala │ │ ├── kubernetes │ │ │ ├── IngressArgs.scala │ │ │ ├── KubernetesArgs.scala │ │ │ ├── PodControllerArgs.scala │ │ │ └── ServiceArgs.scala │ │ └── marathon │ │ │ └── MarathonArgs.scala │ │ ├── concurrent │ │ └── package.scala │ │ ├── docker │ │ ├── Config.scala │ │ ├── DockerCredentials.scala │ │ ├── DockerEngine.scala │ │ ├── DockerRegistry.scala │ │ ├── Image.scala │ │ ├── Manifest.scala │ │ ├── SocketConfig.scala │ │ └── package.scala │ │ ├── files │ │ └── package.scala │ │ ├── http.scala │ │ ├── http │ │ ├── Base64Encoder.scala │ │ ├── Http.scala │ │ ├── HttpHeaders.scala │ │ ├── HttpRequest.scala │ │ ├── HttpResponse.scala │ │ ├── HttpSettings.scala │ │ └── SocketRequest.scala │ │ ├── json.scala │ │ ├── package.scala │ │ ├── process │ │ ├── docker.scala │ │ ├── dockercred.scala │ │ ├── jq.scala │ │ ├── kubectl.scala │ │ └── package.scala │ │ └── runtime │ │ ├── GeneratedResource.scala │ │ ├── kubernetes │ │ ├── AssignedPort.scala │ │ ├── Deployment.scala │ │ ├── GeneratedKubernetesResource.scala │ │ ├── Ingress.scala │ │ ├── Job.scala │ │ ├── Namespace.scala │ │ ├── PodTemplate.scala │ │ ├── Service.scala │ │ └── package.scala │ │ ├── marathon │ │ ├── GeneratedMarathonConfiguration.scala │ │ ├── RpEnvironmentVariables.scala │ │ └── package.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── lightbend │ └── rp │ └── reactivecli │ ├── MinVersionTest.scala │ ├── annotations │ └── AnnotationsTest.scala │ ├── argonaut │ └── YamlRendererTest.scala │ ├── argparse │ └── InputArgsTest.scala │ ├── docker │ ├── ConfigTest.scala │ ├── DockerCredentialsTest.scala │ ├── DockerPackageTest.scala │ ├── DockerRegistryTest.scala │ └── ManifestTest.scala │ ├── http │ └── HttpTest.scala │ └── runtime │ ├── kubernetes │ ├── DeploymentJsonTest.scala │ ├── IngressJsonTest.scala │ ├── JobJsonTest.scala │ ├── KubernetesPackageTest.scala │ ├── NamespaceJsonTest.scala │ └── ServiceJsonTest.scala │ └── marathon │ ├── MarathonPackageTest.scala │ └── RpEnvironmentVariablesTest.scala ├── integration-test └── src │ └── sbt-test │ └── bootstrap-demo │ ├── kubernetes-api │ ├── build.sbt │ ├── kubernetes │ │ └── rbac.mustache │ ├── project │ │ ├── Dependencies.scala │ │ ├── build.properties │ │ └── plugins.sbt │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── application.conf │ │ │ └── scala │ │ │ └── foo │ │ │ ├── ClusterApp.scala │ │ │ └── NoisySingleton.scala │ └── test │ └── kubernetes-dns │ ├── build.sbt │ ├── project │ ├── Dependencies.scala │ ├── build.properties │ └── plugins.sbt │ ├── src │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── foo │ │ ├── ClusterApp.scala │ │ └── NoisySingleton.scala │ └── test ├── project ├── AdditionalIO.scala ├── BintrayExt.scala ├── BuildInfo.scala ├── BuildTarget.scala ├── Properties.scala ├── build.properties └── plugins.sbt ├── script ├── install-minikube.sh └── install-oc.sh └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | logs/ 3 | repository/ 4 | *.lock 5 | *.komodoproject 6 | .DS_Store 7 | project/boot/ 8 | framework/project/boot/ 9 | documentation/api 10 | workspace/ 11 | framework/sbt/boot 12 | .history 13 | .idea 14 | RUNNING_PID 15 | .classpath 16 | .project 17 | .settings/ 18 | .target/ 19 | .cache 20 | *.iml 21 | documentation/*.pdf 22 | framework/test/integrationtest-java/conf/evolutions/ 23 | generated.keystore 24 | generated.truststore 25 | *.log 26 | *.pyc 27 | .venv* 28 | build 29 | dist 30 | project/typesafe.properties 31 | akka-diagnostics 32 | .python-version 33 | 34 | # Eclipse 35 | 36 | .metadata 37 | bin/ 38 | tmp/ 39 | *.tmp 40 | *.bak 41 | *.swp 42 | *~.nib 43 | local.properties 44 | .settings/ 45 | .loadpath 46 | 47 | # Eclipse Core 48 | .project 49 | .externalToolBuilders/ 50 | *.launch 51 | *.pydevproject 52 | .cproject 53 | .classpath 54 | .factorypath 55 | .buildpath 56 | .target 57 | .tern-project 58 | .texlipse 59 | .springBeans 60 | .recommenders/ 61 | .cache-main 62 | .cache-tests 63 | -------------------------------------------------------------------------------- /.sbtopts: -------------------------------------------------------------------------------- 1 | -J-Xms512M 2 | -J-Xmx4096M 3 | -J-Xss2M 4 | -J-XX:MaxMetaspaceSize=1024M 5 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ### Minikube 2 | 3 | on a fresh terminal.. 4 | 5 | ``` 6 | minikube delete 7 | minikube start 8 | eval $(minikube docker-env) 9 | kubectl config current-context 10 | sbt 11 | > integrationTest/scripted bootstrap-demo/dns-kubernetes 12 | ``` 13 | 14 | ### OpenShift 15 | 16 | ``` 17 | oc new-project reactivelibtest1 18 | export OC_TOKEN=$(oc serviceaccounts get-token default) 19 | echo "$OC_TOKEN" | docker login -u unused --password-stdin docker-registry-default.centralpark.lightbend.com 20 | sbt -Ddeckhand.openshift 21 | 22 | > integrationTest/scripted bootstrap-demo/dns-kubernetes 23 | ``` 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactive-cli 2 | 3 | [![Build Status](https://api.travis-ci.org/lightbend/reactive-cli.png?branch=master)](https://travis-ci.org/lightbend/reactive-cli) 4 | 5 | This project is a component of [Lightbend Orchestration](https://developer.lightbend.com/docs/lightbend-orchestration/latest/). Refer to its documentation for usage, examples, and reference information. 6 | 7 | `reactive-cli` is a CLI tool, `rp`, that can inspect Docker images created by [sbt-reactive-app](https://github.com/lightbend/sbt-reactive-app) and generate resources for Kubernetes, DC/OS and potentially other target platforms. 8 | 9 | ## Project Status 10 | 11 | 12 | Lightbend Orchestration is no longer actively developed and will reach its [End of Life](https://developer.lightbend.com/docs/lightbend-platform/introduction/getting-help/support-terminology.html#eol) on April 15, 2020. 13 | 14 | We recommend [Migrating to the Improved Kubernetes Deployment Experience](https://developer.lightbend.com/docs/lightbend-orchestration/current/migration.html). 15 | 16 | ## Installation / Usage 17 | 18 | Consult the [Lightbend Orchestration](https://developer.lightbend.com/docs/lightbend-orchestration/current/setup/cli-installation.html#install-the-cli) documentation for setup and configuration. 19 | 20 | ## Developer 21 | 22 | ### Build setup 23 | 24 | The CLI depends on Scala Native to build, the setup scripts provided in the project follow the instructions on the [Scala Native setup](http://www.scala-native.org/en/latest/user/setup.html#installing-clang-and-runtime-dependencies) page. 25 | 26 | Please ensure you have read through each of the platform-specific setup as there may be manual steps for you to follow. 27 | 28 | The setup script will install the prerequisites listed below. 29 | 30 | ### Prerequisites 31 | 32 | * LVM 3.7+ 33 | * gcc 34 | * libcurl with `curl.h` header file and OpenSSL support 35 | * libgc 36 | * libunwind 37 | * libre2 38 | * nexe (for JS/Windows builds) 39 | 40 | ### npm specific setup 41 | 42 | You'll need `nexe` on your path. One good way of setting up npm to do this is documented on [this StackOverflow post](https://stackoverflow.com/questions/10081293/install-npm-into-home-directory-with-distribution-nodejs-package-ubuntu). 43 | 44 | Once setup, you can use the following command to install nexe: 45 | 46 | ```bash 47 | npm i nexe -g 48 | ``` 49 | 50 | ### macOS specific setup 51 | 52 | Ensure XCode is updated to Apple's latest version. With Apple's latest XCode version, the minimum LLVM version is satisfied, so Homebrew install is not required. 53 | 54 | Once XCode is updated to Apple's latest version, execute the following command to setup the project: 55 | 56 | ```bash 57 | $ brew install bdw-gc re2 jq && \ 58 | brew install curl --with-openssl 59 | ``` 60 | 61 | ### Ubuntu 16 specific setup 62 | 63 | Execute the following command to setup the project: 64 | 65 | ```bash 66 | $ sudo apt-get install -y -qq \ 67 | clang++-3.9 \ 68 | libgc-dev \ 69 | libunwind8-dev \ 70 | libre2-dev \ 71 | libcurl4-openssl-dev \ 72 | jq 73 | ``` 74 | 75 | ### IntelliJ setup 76 | 77 | After importing into IntelliJ, there will be lots of errors. To fix, manually delete the scalalib_native 78 | library from the project libraries, as described [here](https://github.com/twitter/rsc/issues/13#issuecomment-345429964). 79 | This is due to lack of support for sbt-crossproject in IntelliJ. 80 | 81 | ## Building and running 82 | 83 | Use the following sbt command to create the native executable: 84 | 85 | ```bash 86 | $ sbt nativeLink 87 | ``` 88 | 89 | Once built, the native executable can be found in the `cli/target/scala-2.11/rp` path, i.e. 90 | 91 | ```bash 92 | $ cli/native/target/scala-2.11/reactive-cli-out --help 93 | reactive-cli 1.0.0 94 | Usage: reactive-cli [options] 95 | 96 | --help Print this help text 97 | ``` 98 | 99 | ## Packaging 100 | 101 | This project uses a Docker-based build system that builds `.rpm` and `.deb` files inside Docker containers for each 102 | supported distribution. To add a distribution, add a `BuildInfo` instance in `project/BuildInfo.scala` emulating 103 | the ones already created. 104 | 105 | #### Prerequisites 106 | 107 | The build system uses publicly available Docker images that are pushed to Bintray. To rebuild / update these images, 108 | you'll need to run the following: 109 | 110 | ```bash 111 | sbt buildAllDockerImages 112 | ``` 113 | 114 | Afterwards, it will give you the commands you must run to push these images (sbt having tagged them). For example, 115 | below is pushing one of these images: 116 | 117 | ```bash 118 | docker push lightbend-docker-registry.bintray.io/rp/reactive-cli-build-debian-9 119 | ``` 120 | 121 | Note that this doesn't normally need to be done as part of project setup, as the build system will simply pull down 122 | the build images for you. 123 | 124 | #### Building a single distribution package locally 125 | 126 | ``` 127 | sbt "build ubuntu-16-04" 128 | ``` 129 | 130 | #### Building every distribution 131 | 132 | ``` 133 | sbt buildAll 134 | ``` 135 | 136 | Once built, you can find the packages in `target/stage//output`. 137 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | ## Releasing 2 | 3 | Consult the _Lightbend Orchestration Release Process_ document in Google Drive. 4 | -------------------------------------------------------------------------------- /build-mac-osx: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | # this script should probably be implemented in sbt, but as a first step: 4 | 5 | cd "$(dirname "${BASH_SOURCE[0]}")" 6 | 7 | curl="$(cd "$(brew --cellar curl)" && cd "$(ls | tail -n 1)/lib" && pwd)" 8 | 9 | if [ "$curl" = "" ]; then 10 | echo "* missing curl" 11 | exit 1 12 | fi 13 | 14 | version=${1:?usage: build-mac-osx version} 15 | git checkout "tags/v$version" 16 | sbt -J-Xmx2048m -Dbuild.nativeMode=release clean cliNative/test cliNative/package 17 | 18 | pkgbase=target/mac 19 | rm -rf "$pkgbase" 20 | mkdir -p "$pkgbase/bin" "$pkgbase/lib" 21 | 22 | # We bundle curl because the one that ships with macOS doesn't support OpenSSL PEM files 23 | # but that's what Docker uses. 24 | 25 | cp -p "$curl/libcurl.4.dylib" "$pkgbase/lib" 26 | cp -p cli/native/target/scala-*/reactive-cli-out "$pkgbase/bin/rp" 27 | 28 | # link to the bundled curl 29 | install_name_tool -change /usr/lib/libcurl.4.dylib /usr/local/opt/reactive-cli/lib/libcurl.4.dylib "$pkgbase/bin/rp" 30 | 31 | ( cd "$pkgbase" && zip "reactive-cli-${version}-Mac_OS_X-x86_64.zip" bin/* lib/* ) 32 | -------------------------------------------------------------------------------- /build-windows.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | $version = $args[0] 4 | 5 | git checkout "tags/v$version" 6 | 7 | $git_success = $? 8 | 9 | if (-not $git_success) { 10 | throw "Invalid version specified. Specify version as first argument to script" 11 | } 12 | 13 | $env:JAVA_OPTS = "-Xmx4G" 14 | 15 | # FIXME should be running tests, not doing so right now because of line ending issues 16 | sbt clean cliJS/package 17 | 18 | $sbt_success = $? 19 | 20 | if (-not $sbt_success) { 21 | throw "sbt failed" 22 | } 23 | 24 | mkdir target\win 25 | 26 | nexe -o target\win\rp.exe cli\js\target\rp.js 27 | 28 | $nexe_success = $? 29 | 30 | if (-not $nexe_success) { 31 | throw "nexe failed" 32 | } 33 | 34 | Compress-Archive -Path .\target\win\rp.exe -DestinationPath ".\target\win\reactive-cli-$version-Windows-amd64.zip" -------------------------------------------------------------------------------- /cli/build.sbt: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import scala.collection.immutable.Seq 3 | 4 | // Disable GC since the CLI is a short-lived process. 5 | nativeGC := "none" 6 | 7 | nativeLinkingOptions := { 8 | val dynamicLinkerOptions = 9 | Properties 10 | .dynamicLinker 11 | .toVector 12 | .map(dl => s"-Wl,--dynamic-linker=$dl") 13 | 14 | dynamicLinkerOptions ++ Seq( 15 | "-lcurl" 16 | ) ++ sys.props.get("nativeLinkingOptions").fold(Seq.empty[String])(_.split(" ").toVector) 17 | } 18 | 19 | sourceGenerators in Compile += Def.task { 20 | val versionFile = (sourceManaged in Compile).value / "ProgramVersion.scala" 21 | 22 | val versionSource = 23 | """|package com.lightbend.rp.reactivecli 24 | | 25 | |object ProgramVersion { 26 | | val current = "%s" 27 | |} 28 | """.stripMargin.format(version.value) 29 | 30 | IO.write(versionFile, versionSource) 31 | 32 | Seq(versionFile) 33 | } -------------------------------------------------------------------------------- /cli/js/src/main/scala/com/lightbend/rp/reactivecli/Platform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | import com.lightbend.rp.reactivecli.http.{ HttpHeaders, HttpResponse } 20 | import io.scalajs.nodejs.child_process._ 21 | import io.scalajs.nodejs.fs.Fs 22 | import io.scalajs.nodejs.http._ 23 | import io.scalajs.nodejs.https.Https 24 | import io.scalajs.nodejs.os.OS 25 | import io.scalajs.nodejs.path.Path 26 | import io.scalajs.nodejs.process.{ argv, env } 27 | import io.scalajs.nodejs.url.URL 28 | import java.nio.charset.StandardCharsets 29 | import scala.collection.mutable 30 | import scala.concurrent.{ ExecutionContext, Future, Promise } 31 | import scalajs.js 32 | import scalajs.js.URIUtils 33 | import slogging._ 34 | 35 | import js.JSConverters._ 36 | 37 | object Platform extends LazyLogging { 38 | private implicit val ec = executionContext 39 | 40 | def args(supplied: Array[String]): Array[String] = 41 | argv 42 | .toArray 43 | .drop(2) 44 | 45 | def deleteFile(path: String): Unit = 46 | Fs.unlinkSync(path) 47 | 48 | def environment(): Map[String, String] = 49 | env.toMap 50 | 51 | def executionContext: ExecutionContext = 52 | scala.concurrent.ExecutionContext.Implicits.global 53 | 54 | def fileExists(path: String): Boolean = 55 | Fs.existsSync(path) 56 | 57 | def httpRequest(request: http.HttpRequest)(implicit settings: http.HttpSettings): Future[http.HttpResponse] = { 58 | val url = URL.parse(request.requestUrl) 59 | 60 | val options = js.Dynamic.literal( 61 | method = request.requestMethod, 62 | hostname = url.hostname, 63 | port = if (url.port == null) null else url.port.get.toInt, 64 | path = url.path, 65 | protocol = url.protocol, 66 | headers = request.requestHeaders.headers.toJSDictionary.asInstanceOf[js.Object]) 67 | 68 | settings 69 | .tlsCacertsPath 70 | .map(readFile) 71 | .foreach(options.ca= _) 72 | 73 | settings 74 | .tlsCertPath 75 | .map(readFile) 76 | .foreach(options.cert= _) 77 | 78 | settings 79 | .tlsKeyPath 80 | .map(readFile) 81 | .foreach(options.key= _) 82 | 83 | val promise = Promise[http.HttpResponse] 84 | 85 | val handler = { (resp: ServerResponse) => 86 | var data: String = null 87 | 88 | resp.setEncoding("utf8") 89 | 90 | resp.onData { d => 91 | if (data == null) { 92 | data = "" 93 | } 94 | 95 | data += d 96 | } 97 | 98 | resp.onEnd { () => 99 | promise.success( 100 | HttpResponse( 101 | resp.statusCode, 102 | HttpHeaders(resp.headers.toMap), 103 | Option(data))) 104 | } 105 | } 106 | 107 | if (options.protocol.asInstanceOf[String].contains("https:")) 108 | Https.get(options.asInstanceOf[RequestOptions], handler) 109 | else 110 | Http.get(options.asInstanceOf[RequestOptions], handler) 111 | 112 | promise.future 113 | } 114 | 115 | def encodeURI(uri: String): String = URIUtils.encodeURI(uri) 116 | 117 | def mkDirs(path: String): Unit = 118 | Fs.mkdirSync(path) 119 | 120 | def parentFor(path: String): String = 121 | Path.dirname(path) 122 | 123 | def pathFor(components: String*): String = 124 | if (components.isEmpty) 125 | "" 126 | else 127 | Path.join(components.head, components.tail: _*) 128 | 129 | def processExec(args: Seq[String], stdinFile: Option[String] = None): Future[(Int, String)] = { 130 | if (args.isEmpty) { 131 | Future.successful(1 -> "") 132 | } else { 133 | val result = Promise[(Int, String)] 134 | 135 | var output = mutable.ArrayBuilder.make[Byte]() 136 | 137 | val processOptions = js.Dynamic.literal(windowsHide = false) 138 | 139 | val process = ChildProcess.spawn(args.head, args.tail.toJSArray, processOptions).asInstanceOf[js.Dynamic] 140 | 141 | if (stdinFile.isDefined) { 142 | process.stdin.setEncoding("utf-8") 143 | process.stdin.write(readFile(stdinFile.get)) 144 | process.stdin.end() 145 | } 146 | 147 | process.stdout.on("data", { (data: js.Array[Byte]) => 148 | output ++= data 149 | }) 150 | 151 | process.stderr.on("data", { (data: js.Array[Byte]) => 152 | output ++= data 153 | }) 154 | 155 | process.on("exit", { (code: Int, signal: Int) => 156 | result.success((code, new String(output.result(), StandardCharsets.UTF_8))) 157 | }) 158 | 159 | process.on("error", { () => 160 | // @FIXME log the error? 161 | 162 | result.success(127 -> "") 163 | }) 164 | 165 | result.future 166 | } 167 | } 168 | 169 | def readFile(path: String): String = 170 | Fs.readFileSync(path, "utf8") 171 | 172 | def stop(): Unit = () 173 | 174 | def start(): Unit = { 175 | LoggerConfig.factory = PrintLoggerFactory() 176 | } 177 | 178 | def withTempDir[T](f: String => Future[T]): Future[T] = { 179 | val dir = Fs.mkdtempSync(pathFor(OS.tmpdir(), "reactive-cli")) 180 | 181 | def clean(): Unit = { 182 | Fs.readdirSync(dir).foreach { file => 183 | Fs.unlinkSync(pathFor(dir, file)) 184 | } 185 | 186 | Fs.rmdirSync(dir) 187 | } 188 | 189 | try { 190 | val future = f(dir) 191 | 192 | future.onComplete(_ => clean()) 193 | 194 | future 195 | } catch { 196 | case t: Throwable => 197 | clean() 198 | 199 | throw t 200 | } 201 | } 202 | 203 | def withTempFile[T](f: String => Future[T]): Future[T] = { 204 | val dir = Fs.mkdtempSync(pathFor(OS.tmpdir(), "reactive-cli")) 205 | val file = pathFor(dir, "file.temp") 206 | 207 | writeFile(file, "") 208 | 209 | def clean(): Unit = { 210 | try { 211 | deleteFile(file) 212 | } catch { 213 | case t: Throwable => logger.debug(s"Failed to remove $file: ${t.getMessage}") 214 | } 215 | 216 | try { 217 | Fs.rmdirSync(dir) 218 | } catch { 219 | case t: Throwable => logger.debug(s"Failed to remove $dir: ${t.getMessage}") 220 | } 221 | } 222 | 223 | try { 224 | val future = f(file) 225 | 226 | future.onComplete(_ => clean()) 227 | 228 | future 229 | } catch { 230 | case t: Throwable => 231 | clean() 232 | 233 | throw t 234 | } 235 | } 236 | 237 | def writeFile(path: String, data: String): Unit = Fs.writeFileSync(path, data) 238 | } -------------------------------------------------------------------------------- /cli/native/src/main/scala/com/lightbend/rp/reactivecli/Platform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | import java.nio.charset.StandardCharsets 20 | import java.nio.file.{ Files, Paths } 21 | import java.net.URI 22 | import scala.concurrent.{ ExecutionContext, Future } 23 | import scala.collection.JavaConverters._ 24 | import scala.util.{ Failure, Success, Try } 25 | import slogging._ 26 | 27 | object Platform extends LazyLogging { 28 | private implicit val ec = executionContext 29 | 30 | def args(supplied: Array[String]): Array[String] = 31 | supplied 32 | 33 | def deleteFile(path: String): Unit = 34 | Files.delete(Paths.get(path)) 35 | 36 | def environment(): Map[String, String] = 37 | sys.env 38 | 39 | /** 40 | * This turns out to be necessary because of Scala Native and uTest. Even though no calls 41 | * in our native codebase are async (all Future.successful), when you map/flatMap 42 | * that operation gets run on an ExecutionContext. We provide this one to ensure 43 | * all of those operations are done on the same thread. Thus, when uTest does 44 | * a call to `Await.result(future, Duration.Inf)` the future is already completed 45 | * and doesn't fail. 46 | * 47 | * @return 48 | */ 49 | lazy val executionContext: ExecutionContext = 50 | new scala.concurrent.ExecutionContext { 51 | def execute(runnable: Runnable) = 52 | try { 53 | runnable.run() 54 | } catch { 55 | 56 | case t: Throwable => throw t 57 | } 58 | 59 | def reportFailure(t: Throwable) = { 60 | Console.err.println("Failure in RunNow async execution: " + t) 61 | Console.err.println(t.getStackTrace.mkString("\n")) 62 | } 63 | } 64 | 65 | def fileExists(path: String): Boolean = 66 | Files.exists(Paths.get(path)) 67 | 68 | def httpRequest(request: http.HttpRequest)(implicit settings: http.HttpSettings): Future[http.HttpResponse] = 69 | http.NativeHttp(request) match { 70 | case Failure(t) => Future.failed(t) 71 | case Success(r) => Future.successful(r) 72 | } 73 | 74 | def encodeURI(uri: String): String = { 75 | val enc = new URI(uri.replace(" ", "+")) 76 | enc.toASCIIString 77 | } 78 | 79 | def mkDirs(path: String): Unit = 80 | Files.createDirectories(Paths.get(path)) 81 | 82 | def parentFor(path: String): String = 83 | Paths.get(path).getParent.toString 84 | 85 | def pathFor(components: String*): String = 86 | if (components.isEmpty) 87 | "" 88 | else 89 | Paths.get(components.head, components.tail: _*).toString 90 | 91 | def processExec(args: Seq[String], stdinFile: Option[String] = None): Future[(Int, String)] = 92 | process.NativeProcess.exec(args, stdinFile) 93 | 94 | def readFile(path: String): String = 95 | new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8) 96 | 97 | def stop(): Unit = { 98 | http.NativeHttp.globalCleanup() 99 | } 100 | 101 | def start(): Unit = { 102 | LoggerConfig.factory = TerminalLoggerFactory 103 | 104 | http.NativeHttp.globalInit().get 105 | } 106 | 107 | def withTempDir[T](f: String => Future[T]): Future[T] = { 108 | val dir = Files.createTempDirectory("reactive-cli") 109 | 110 | def clean(): Unit = { 111 | Files 112 | .list(dir) 113 | .iterator() 114 | .asScala 115 | .foreach(Files.delete) 116 | 117 | Files.delete(dir) 118 | } 119 | 120 | try { 121 | val future = f(dir.toString) 122 | 123 | future.onComplete(_ => clean()) 124 | 125 | future 126 | } catch { 127 | case t: Throwable => 128 | clean() 129 | 130 | throw t 131 | } 132 | } 133 | 134 | def withTempFile[T](f: String => Future[T]): Future[T] = { 135 | val file = Files.createTempFile("reactive-cli", ".temp").toString 136 | 137 | def clean(): Unit = { 138 | try { 139 | deleteFile(file) 140 | } catch { 141 | case t: Throwable => logger.debug(s"Failed to remove $file: ${t.getMessage}") 142 | } 143 | } 144 | 145 | try { 146 | val future = f(file) 147 | 148 | future.onComplete(_ => clean()) 149 | 150 | future 151 | } catch { 152 | case t: Throwable => 153 | clean() 154 | 155 | throw t 156 | } 157 | } 158 | 159 | def writeFile(path: String, data: String): Unit = 160 | Files.write(Paths.get(path), data.getBytes(StandardCharsets.UTF_8)) 161 | } -------------------------------------------------------------------------------- /cli/native/src/main/scala/com/lightbend/rp/reactivecli/http/NativeHttp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | import scala.scalanative.native 20 | import scala.util.{ Failure, Success, Try } 21 | import scala.util.matching.Regex 22 | import scala.collection.immutable.Seq 23 | 24 | import slogging._ 25 | 26 | object NativeHttp extends LazyLogging { 27 | type HttpExchange = HttpRequest => Try[HttpResponse] 28 | 29 | private val CRLF = "\r\n" 30 | private val HttpHeaderAndBodyPartsSeparator = CRLF + CRLF 31 | private val HttpHeaderNameAndValueSeparator = ":" 32 | 33 | case class InternalNativeFailure(errorCode: Long, errorDescription: String) extends RuntimeException(s"$errorCode: $errorDescription") 34 | 35 | val defaultSettings = HttpSettings() 36 | 37 | def http(implicit settings: HttpSettings): HttpExchange = apply 38 | 39 | /** 40 | * Initializes libcurl` internal state by calling `curl_global_init` underneath. 41 | * This method is _NOT_ thread safe and it's meant to be called at the start of the program. 42 | */ 43 | def globalInit(): Try[Unit] = 44 | native.Zone { implicit z => 45 | val errorCode = nativebinding.http.global_init() 46 | if (errorCode.toInt == 0) 47 | Success(Unit) 48 | else 49 | Failure(InternalNativeFailure(-70, "Failure calling curl_global_init")) 50 | } 51 | 52 | /** 53 | * Performs cleanup of libcurl` internal state by calling `curl_global_cleanup` underneath. 54 | * This method is _NOT_ thread safe and it's meant to be called before termination of the program. 55 | */ 56 | def globalCleanup(): Unit = 57 | native.Zone { implicit z => 58 | nativebinding.http.global_cleanup() 59 | } 60 | 61 | def apply(request: HttpRequest)(implicit settings: HttpSettings): Try[HttpResponse] = 62 | doHttp( 63 | request.requestMethod, 64 | request.requestUrl, 65 | request.headersWithAuth.headers, 66 | request.requestBody, 67 | request.tlsValidationEnabled, 68 | Nil) 69 | 70 | private def doHttp( 71 | method: String, 72 | url: String, 73 | headers: Map[String, String], 74 | requestBody: Option[String], 75 | tlsValidationEnabled: Option[Boolean], 76 | visitedUrls: List[String])(implicit settings: HttpSettings): Try[HttpResponse] = 77 | native.Zone { implicit z => 78 | val isTlsValidationEnabled = tlsValidationEnabled.getOrElse(settings.tlsValidationEnabled) 79 | 80 | val response = nativebinding.http.do_http( 81 | validate_tls = if (isTlsValidationEnabled) 1 else 0, 82 | method, 83 | url, 84 | httpHeadersToDelimitedString(headers), 85 | requestBody.getOrElse(""), 86 | settings.tlsCacertsPath.fold("")(_.toString), 87 | settings.tlsCertPath.fold("")(_.toString), 88 | settings.tlsKeyPath.fold("")(_.toString)) 89 | 90 | response.error match { 91 | case 0 => { 92 | val hs = HttpHeaders(parseHeaders(response.header)) 93 | Success(HttpResponse(response.status, hs, response.body)) 94 | } 95 | case -1 => { 96 | Failure(InternalNativeFailure(response.error, s"no response from $url")) 97 | } 98 | case _ => { 99 | val msg = nativebinding.http.error_message(response.error) 100 | Failure(InternalNativeFailure(response.error, msg)) 101 | } 102 | } 103 | } 104 | 105 | private def httpHeadersToDelimitedString(headers: Map[String, String]): Seq[String] = 106 | headers 107 | .map { 108 | case (headerName, headerValue) => s"$headerName$HttpHeaderNameAndValueSeparator $headerValue" 109 | }.toVector 110 | 111 | def parseHeaders(input: Option[String]): Map[String, String] = { 112 | val matchHeader = """^([a-zA-Z-_]+):(.*)$""".r 113 | 114 | input match { 115 | case Some(headers) => 116 | // Filter out empty lines 117 | val splitHeader = headers.split(CRLF).filter(!_.isEmpty) 118 | val protocol = splitHeader.head 119 | if (!protocol.startsWith("HTTP/1.1") && !protocol.startsWith("HTTP/2")) 120 | logger.debug("Unexpected protocol name: \"{}\"", protocol) 121 | 122 | // Exclude the first line which is the HTTP status line 123 | val headerLines = splitHeader.tail 124 | 125 | // Keep track of previous header name to handle multiline fields correctly 126 | var prev: Option[String] = None 127 | headerLines.foldLeft(Map.empty[String, String]) { (v, l) => 128 | if (l.startsWith(" ") || l.startsWith("\t")) { 129 | prev match { 130 | case None => { 131 | logger.debug("Unexpected whitespace in HTTP header: \"{}\"", l) 132 | v 133 | } 134 | case Some(key) => 135 | val prevVal = v(key) 136 | v.updated(key, prevVal + " " + l.trim) 137 | } 138 | } else { 139 | l match { 140 | case matchHeader(name, value) => { 141 | prev = Some(name) 142 | v.updated(name, if (value == null) "" else value.trim) 143 | } 144 | case _ => { 145 | logger.debug("Cannot parse HTTP header: \"{}\"", l) 146 | v 147 | } 148 | } 149 | } 150 | } 151 | case _ => 152 | Map.empty[String, String] 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /cli/native/src/main/scala/com/lightbend/rp/reactivecli/http/nativebinding/nativebinding.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http.nativebinding 18 | 19 | import scala.scalanative.native 20 | import scala.scalanative.native._ 21 | 22 | object http { 23 | 24 | case class HttpResponse(error: Long, status: Long, header: Option[String], body: Option[String]) 25 | 26 | def global_init(): CInt = { 27 | val res = curl.global_init(curl.CURLGlobals.CURL_GLOBAL_DEFAULT) 28 | if (res == curl.CURLcode.CURLE_OK) 0 29 | else { 30 | System.err.println("curl_global_init() failed: " + curl.easy_strerror(res).toString) 31 | -1 32 | } 33 | } 34 | 35 | def global_cleanup(): Unit = { 36 | curl.global_cleanup() 37 | } 38 | 39 | def do_http(validate_tls: CLong, http_method: String, 40 | url: String, request_headers: Seq[String], 41 | request_body: String, tls_cacerts_path: String, 42 | ssl_cert: String, ssl_key: String)(implicit z: Zone): HttpResponse = { 43 | 44 | var result = HttpResponse(-1L, 0L, None, None) 45 | val req = curl.easy_init() 46 | 47 | // For debugging 48 | //curl.easy_setopt(req, curl.CURLoption.CURLOPT_VERBOSE, 1L) 49 | 50 | // Set up options 51 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_URL, toCString(url)) 52 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_CUSTOMREQUEST, toCString(http_method)) 53 | if (validate_tls == 0) 54 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_SSL_VERIFYPEER, 0L) 55 | if (tls_cacerts_path.length > 0) 56 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_CAINFO, toCString(tls_cacerts_path)) 57 | if (ssl_cert.length > 0) 58 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_SSLCERT, toCString(ssl_cert)) 59 | if (ssl_key.length > 0) 60 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_SSLKEY, toCString(ssl_key)) 61 | if (request_body.length > 0) 62 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_POSTFIELDS, toCString(request_body)) 63 | 64 | // Set up http headers 65 | if (request_headers.length > 0) { 66 | // The following code should work here when scala-native Ptr[T] context escape bug is fixed: 67 | // https://github.com/scala-native/scala-native/issues/367 68 | 69 | /* 70 | var head = 0.cast[Ptr[curl.curl_slist]] 71 | request_headers.foreach(header => { 72 | val next = alloc[curl.curl_slist] 73 | !next._1 = toCString(header) 74 | !next._2 = head.cast[Ptr[Byte]] 75 | head = next 76 | 77 | }) 78 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_HTTPHEADER, head) 79 | */ 80 | 81 | // FIXME: Hand-unrolled code as a workaround for Ptr[T] bug, at most 2 headers are used in client code. 82 | if (request_headers.length == 1) { 83 | val list = stackalloc[curl.curl_slist] 84 | !list._1 = toCString(request_headers.head) 85 | !list._2 = 0.cast[Ptr[Byte]] 86 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_HTTPHEADER, list) 87 | } else if (request_headers.length == 2) { 88 | val list = stackalloc[curl.curl_slist] 89 | val list_next = stackalloc[curl.curl_slist] 90 | !list._1 = toCString(request_headers.head) 91 | !list._2 = list_next.cast[Ptr[Byte]] 92 | !list_next._1 = toCString(request_headers.tail.head) 93 | !list_next._2 = 0.cast[Ptr[Byte]] 94 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_HTTPHEADER, list) 95 | } else { 96 | System.err.println("Unable to put more than 2 http request headers at this time") 97 | } 98 | } 99 | 100 | val body = new StringBuilder() 101 | val header = new StringBuilder() 102 | 103 | def writefunc(ptr: Ptr[Byte], size: CSize, nmemb: CSize, builder: StringBuilder): CSize = { 104 | val realsize: CSize = size * nmemb 105 | var i = 0 106 | while (i < realsize) { 107 | builder.append(ptr(i).toChar) 108 | i += 1 109 | } 110 | realsize 111 | } 112 | 113 | val writecallback = CFunctionPtr.fromFunction4(writefunc) 114 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_WRITEFUNCTION, writecallback) 115 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_HEADERFUNCTION, writecallback) 116 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_WRITEDATA, body) 117 | curl.easy_setopt(req, curl.CURLoption.CURLOPT_HEADERDATA, header) 118 | 119 | def buildString(b: StringBuilder): Option[String] = { 120 | if (b.length > 0) Some(b.toString) 121 | else None 122 | } 123 | 124 | // Perform request 125 | val res = curl.easy_perform(req) 126 | if (res == curl.CURLcode.CURLE_OK) { 127 | // Zone here shouldn't be needed but it functions as a workaround for scala-native bug. 128 | // Without zone here, compiler crashes: 129 | // [error] (cli/compile:nativeOptimizeNIR) java.util.NoSuchElementException: key not found: Local(167) 130 | Zone { implicit z => 131 | val response_code = stackalloc[CLong] 132 | curl.easy_getinfo(req, curl.CURLINFO.RESPONSE_CODE, response_code) 133 | result = HttpResponse(0L, !response_code, buildString(header), buildString(body)) 134 | } 135 | } 136 | 137 | curl.easy_cleanup(req) 138 | 139 | result 140 | } 141 | 142 | def error_message(error_code: Long): String = { 143 | fromCString(curl.easy_strerror(new curl.CURLcode(error_code.toInt))) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /cli/native/src/main/scala/com/lightbend/rp/reactivecli/process/NativeProcess.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.process 18 | 19 | import com.lightbend.rp.reactivecli.files._ 20 | import scala.concurrent.Future 21 | import scalanative.native._ 22 | import slogging._ 23 | 24 | object NativeProcess extends LazyLogging { 25 | def exec(args: Seq[String], stdinFile: Option[String] = None): Future[(Int, String)] = 26 | withTempFile { outputFile => 27 | Zone { implicit z => 28 | val cmd = if (stdinFile.isDefined) 29 | s"${command(args)} < ${stdinFile.get} > $outputFile 2>&1" 30 | else 31 | s"${command(args)} > $outputFile 2>&1" 32 | 33 | val code = stdlib.system(toCString(cmd)) 34 | val output = readFile(outputFile) 35 | 36 | Future.successful(code -> output) 37 | } 38 | } 39 | 40 | /** 41 | * Prepares a sequence of arguments to be passed to system(). We assume a POSIX target for now, which means 42 | * the command will be processed by `sh` per POSIX specification. 43 | * 44 | * This means that we can simply enclose each argument in a single quote. However, if a single quote occurs in 45 | * an argument, we special case that by enclosing it in double quotes. Also, we don't enclose "|" pipes and 46 | * stdin/stdout redirects. 47 | */ 48 | private[NativeProcess] def command(args: Seq[String]): String = { 49 | def escape(s: String): String = 50 | "'" + s.replaceAllLiterally("'", "'\"'\"'") + "'" 51 | 52 | args 53 | .map(escape) 54 | .mkString(" ") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cli/native/src/test/scala/com/lightbend/rp/reactivecli/http/NativeHttpTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | import scala.collection.immutable.Map 20 | import utest._ 21 | 22 | object NativeHttpTest extends TestSuite { 23 | val tests = this{ 24 | "Parse HTTP headers" - { 25 | // No header field value 26 | assert(NativeHttp.parseHeaders(Some("HTTP/1.1 200 OK\r\nAccept:")) == Map( 27 | "Accept" -> "")) 28 | 29 | // Real-world punctuation 30 | assert(NativeHttp.parseHeaders(Some( 31 | """HTTP/1.1 200 OK 32 | |Date: Mon, 07 May 2018 08:43:13 GMT 33 | |Expires: -1 34 | |Cache-Control: private, max-age=0 35 | |Content-Type: text/html; charset=ISO-8859-1""".stripMargin.replaceAll("\n", "\r\n"))) == Map( 36 | "Date" -> "Mon, 07 May 2018 08:43:13 GMT", 37 | "Expires" -> "-1", 38 | "Cache-Control" -> "private, max-age=0", 39 | "Content-Type" -> "text/html; charset=ISO-8859-1")) 40 | 41 | // Multiline field values 42 | assert(NativeHttp.parseHeaders(Some( 43 | """HTTP/1.1 200 OK 44 | |Date: Mon, 07 May 2018 45 | | 08:43:13 GMT 46 | |Expires: -1 47 | |Cache-Control: private, 48 | | max-age=0 49 | |Content-Type: text/html; charset=ISO-8859-1""".stripMargin.replaceAll("\n", "\r\n"))) == Map( 50 | "Date" -> "Mon, 07 May 2018 08:43:13 GMT", 51 | "Expires" -> "-1", 52 | "Cache-Control" -> "private, max-age=0", 53 | "Content-Type" -> "text/html; charset=ISO-8859-1")) 54 | } 55 | 56 | "Parse headers not following HTTP spec" - { 57 | // Empty lines before and after HTTP status 58 | assert(NativeHttp.parseHeaders(Some("\r\n\r\n\r\nHTTP/1.1 200 OK\r\n \r\nAccept:\r\n")) == Map( 59 | "Accept" -> "")) 60 | 61 | // No colon separator 62 | assert(NativeHttp.parseHeaders(Some( 63 | """HTTP/1.1 200 OK 64 | |Accept: * 65 | |Date Mon, 07 May 2018 08:43:13 GMT 66 | |Expires: -1""".stripMargin.replaceAll("\n", "\r\n"))) == Map( 67 | "Accept" -> "*", 68 | "Expires" -> "-1")) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /cli/native/src/test/scala/com/lightbend/rp/reactivecli/http/PlatformTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | import com.lightbend.rp.reactivecli.Platform 20 | import utest._ 21 | 22 | object PlatformTest extends TestSuite { 23 | val tests = this{ 24 | "Encode URI" - { 25 | // No header field value 26 | assert(Platform.encodeURI("https://exampl.com?service=Docker Registry") == "https://exampl.com?service=Docker+Registry") 27 | 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/Annotation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations 18 | 19 | case class Annotation(key: String, value: String) 20 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/Endpoint.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations 18 | 19 | import scala.collection.immutable.Seq 20 | 21 | sealed trait Endpoint { 22 | def index: Int 23 | def name: String 24 | def port: Int 25 | } 26 | 27 | case class HttpEndpoint(index: Int, name: String, port: Int, ingress: Seq[HttpIngress]) extends Endpoint 28 | 29 | case class TcpEndpoint(index: Int, name: String, port: Int) extends Endpoint 30 | 31 | case class UdpEndpoint(index: Int, name: String, port: Int) extends Endpoint 32 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/EnvironmentVariable.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations 18 | 19 | private[reactivecli] trait EnvironmentVariable 20 | 21 | case class LiteralEnvironmentVariable(value: String) extends EnvironmentVariable 22 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/Ingress.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations 18 | 19 | import scala.collection.immutable.Seq 20 | 21 | sealed trait Ingress { 22 | def ingressPorts: Seq[Int] 23 | } 24 | 25 | case class PortIngress(ingressPorts: Seq[Int]) extends Ingress 26 | 27 | case class HttpIngress(ingressPorts: Seq[Int], hosts: Seq[String], paths: Seq[String]) extends Ingress 28 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/Module.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations 18 | 19 | object Module { 20 | val AkkaClusterBootstrapping = "akka-cluster-bootstrapping" 21 | val AkkaManagement = "akka-management" 22 | val Common = "common" 23 | val PlayHttpBinding = "play-http-binding" 24 | val Secrets = "secrets" 25 | val ServiceDiscovery = "service-discovery" 26 | val Status = "status" 27 | } 28 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/Secret.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations 18 | 19 | case class Secret(name: String, key: String) -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/kubernetes/ConfigMapEnvironmentVariable.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations.kubernetes 18 | 19 | import com.lightbend.rp.reactivecli.annotations.EnvironmentVariable 20 | 21 | case class ConfigMapEnvironmentVariable(mapName: String, key: String) extends EnvironmentVariable -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/kubernetes/FieldRefEnvironmentVariable.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations.kubernetes 18 | 19 | import com.lightbend.rp.reactivecli.annotations.EnvironmentVariable 20 | 21 | case class FieldRefEnvironmentVariable(fieldPath: String) extends EnvironmentVariable 22 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/kubernetes/SecretKeyRefEnvironmentVariable.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.annotations.kubernetes 18 | 19 | import com.lightbend.rp.reactivecli.annotations.EnvironmentVariable 20 | 21 | case class SecretKeyRefEnvironmentVariable(name: String, key: String) extends EnvironmentVariable 22 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/annotations/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | package object annotations { 20 | val legacyAkkaManagementPortName = "akka-mgmt-http" 21 | } 22 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/argonaut/YamlRenderer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argonaut 18 | 19 | import argonaut.Argonaut._ 20 | import argonaut._ 21 | 22 | object YamlRenderer { 23 | private val Indent = " " 24 | private val IndentArray = "- " 25 | 26 | def render(j: Json): String = { 27 | def string(string: JsonString): String = 28 | if (needsQuotes(string)) 29 | jString(string).nospaces 30 | else 31 | string 32 | 33 | def bool(b: Boolean): String = 34 | if (b) "true" else "false" 35 | 36 | def `null`: String = 37 | "null" 38 | 39 | def number(number: JsonNumber): String = 40 | number.asJson.nospaces 41 | 42 | def nonEmptyArrayOrObject(j: Json): Boolean = 43 | (j.isArray && j.array.get.nonEmpty) || (j.isObject && j.obj.get.isNotEmpty) 44 | 45 | def array(array: JsonArray, level: Int): String = 46 | if (array.isEmpty) 47 | "[]" 48 | else 49 | array 50 | .map { element => 51 | val rendered = render(element, level + 1) 52 | 53 | if (nonEmptyArrayOrObject(element)) { 54 | val initialSpaces = spaces(level + 1) 55 | val replacement = spaces(level) + IndentArray 56 | val fixed = rendered.replaceFirst(initialSpaces, replacement) 57 | 58 | fixed 59 | } else { 60 | spaces(level) + IndentArray + rendered 61 | } 62 | } 63 | .mkString("\n") 64 | 65 | def obj(obj: JsonObject, level: Int): String = 66 | if (obj.isEmpty) 67 | "{}" 68 | else 69 | obj 70 | .toList 71 | .map { 72 | case (field, value) => 73 | val renderedField = string(field) 74 | val rendered = render(value, level + 1) 75 | val separator = 76 | if (nonEmptyArrayOrObject(value)) 77 | ":\n" 78 | else 79 | ": " 80 | 81 | spaces(level) + renderedField + separator + rendered 82 | } 83 | .mkString("\n") 84 | 85 | def render(json: Json, level: Int): String = { 86 | if (json.isString) 87 | string(json.string.get) 88 | else if (json.isBool) 89 | bool(json.bool.get) 90 | else if (json.isNull) 91 | `null` 92 | else if (json.isNumber) 93 | number(json.number.get) 94 | else if (json.isArray) 95 | array(json.array.get, level) 96 | else if (json.isObject) 97 | obj(json.obj.get, level) 98 | else 99 | throw new IllegalArgumentException(s"Unexpected state for $json") 100 | } 101 | 102 | render(json = j, level = 0) 103 | } 104 | 105 | private def spaces(level: Int): String = Indent * level 106 | 107 | private def needsQuotes(string: String) = { 108 | val alwaysQuote = Set("null", "true", "false") 109 | 110 | string.isEmpty || string.trim != string || !string.matches("^[A-Za-z][A-Za-z0-9 ]*$") || alwaysQuote.contains(string.toLowerCase) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/argparse/CommandArgs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argparse 18 | 19 | import scala.collection.immutable.Seq 20 | import GenerateDeploymentArgs.{ DockerRegistryUseHttpsDefault, DockerRegistryValidateTlsDefault, DockerRegistryUseLocalDefault } 21 | 22 | /** 23 | * Base type which represents input argument for a specific command invoked by the user. 24 | */ 25 | sealed trait CommandArgs 26 | 27 | object VersionArgs extends CommandArgs 28 | 29 | object GenerateDeploymentArgs { 30 | val DockerRegistryUseHttpsDefault = true 31 | val DockerRegistryValidateTlsDefault = true 32 | val DockerRegistryUseLocalDefault = true 33 | 34 | /** 35 | * Convenience method to set the [[GenerateDeploymentArgs]] values when parsing the complete user input. 36 | * Refer to [[InputArgs.parser()]] for more details. 37 | */ 38 | def set[T](f: (T, GenerateDeploymentArgs) => GenerateDeploymentArgs): (T, InputArgs) => InputArgs = 39 | (value, args) => { 40 | args.commandArgs match { 41 | case Some(v: GenerateDeploymentArgs) => 42 | val updates = f.apply(value, v) 43 | args.copy(commandArgs = Some(updates)) 44 | 45 | case _ => args 46 | } 47 | } 48 | } 49 | 50 | object DeploymentType { 51 | val Canary = "canary" 52 | val BlueGreen = "blue-green" 53 | val Rolling = "rolling" 54 | val All = Seq(Canary, BlueGreen, Rolling) 55 | } 56 | 57 | sealed trait DeploymentType 58 | 59 | case object CanaryDeploymentType extends DeploymentType 60 | case object BlueGreenDeploymentType extends DeploymentType 61 | case object RollingDeploymentType extends DeploymentType 62 | 63 | /** 64 | * Represents the discovery method during Akka Boostrap on Kubernetes. 65 | */ 66 | sealed trait DiscoveryMethod 67 | object DiscoveryMethod { 68 | case object KubernetesApi extends DiscoveryMethod { 69 | override def toString: String = "kubernetes-api" 70 | } 71 | case object AkkaDns extends DiscoveryMethod { 72 | override def toString = "akka-dns" 73 | } 74 | def all = Seq(AkkaDns, KubernetesApi) 75 | } 76 | 77 | /** 78 | * Represents the input argument for `generate-deployment` command. 79 | */ 80 | case class GenerateDeploymentArgs( 81 | application: Option[String] = None, 82 | akkaClusterJoinExisting: Boolean = false, 83 | akkaClusterSkipValidation: Boolean = false, 84 | deploymentType: DeploymentType = CanaryDeploymentType, 85 | discoveryMethod: DiscoveryMethod = DiscoveryMethod.KubernetesApi, 86 | dockerImages: Seq[String] = Seq.empty, 87 | name: Option[String] = None, 88 | version: Option[String] = None, 89 | environmentVariables: Map[String, String] = Map.empty, 90 | cpu: Option[Double] = None, 91 | memory: Option[Long] = None, 92 | diskSpace: Option[Long] = None, 93 | targetRuntimeArgs: Option[TargetRuntimeArgs] = None, 94 | registryUsername: Option[String] = None, 95 | registryPassword: Option[String] = None, 96 | registryUseHttps: Boolean = DockerRegistryUseHttpsDefault, 97 | registryValidateTls: Boolean = DockerRegistryValidateTlsDefault, 98 | registryUseLocal: Boolean = DockerRegistryUseLocalDefault, 99 | externalServices: Map[String, Seq[String]] = Map.empty) extends CommandArgs 100 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/argparse/TargetRuntimeArgs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argparse 18 | 19 | /** 20 | * Base type to indicate target runtime specific argument, i.e. 21 | * [[com.lightbend.rp.reactivecli.argparse.kubernetes.KubernetesArgs]] extends [[TargetRuntimeArgs]] which represents 22 | * Kubernetes specific input arguments. 23 | */ 24 | class TargetRuntimeArgs private[argparse] () 25 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/argparse/kubernetes/IngressArgs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argparse.kubernetes 18 | 19 | import com.lightbend.rp.reactivecli.argparse.InputArgs 20 | import scala.collection.immutable.Seq 21 | import scala.concurrent.Future 22 | 23 | object IngressArgs { 24 | /** 25 | * Convenience method to set the [[IngressArgs]] values when parsing the complete user input. 26 | * Refer to [[com.lightbend.rp.reactivecli.argparse.InputArgs.parser()]] for more details. 27 | */ 28 | def set[T](f: (T, IngressArgs) => IngressArgs): (T, InputArgs) => InputArgs = { (val1: T, inputArgs: InputArgs) => 29 | KubernetesArgs 30 | .set { (val2: T, kubernetesArgs) => 31 | kubernetesArgs.copy( 32 | ingressArgs = f(val2, kubernetesArgs.ingressArgs)) 33 | } 34 | .apply(val1, inputArgs) 35 | } 36 | } 37 | 38 | /** 39 | * Represents user input arguments required to build Kubernetes Ingress resource. 40 | */ 41 | case class IngressArgs( 42 | apiVersion: Future[String] = KubernetesArgs.DefaultIngressApiVersion, 43 | hosts: Seq[String] = Seq.empty, 44 | ingressAnnotations: Map[String, String] = Map.empty, 45 | name: Option[String] = None, 46 | pathAppend: Option[String] = None, 47 | tlsSecrets: Seq[String] = Seq.empty) 48 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/argparse/kubernetes/KubernetesArgs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argparse.kubernetes 18 | 19 | import com.lightbend.rp.reactivecli.argparse.{ GenerateDeploymentArgs, InputArgs, TargetRuntimeArgs } 20 | import com.lightbend.rp.reactivecli.json.JsonTransformExpression 21 | import com.lightbend.rp.reactivecli.process.kubectl 22 | import com.lightbend.rp.reactivecli.runtime.kubernetes.PodTemplate 23 | import java.io.PrintStream 24 | import scala.concurrent.Future 25 | 26 | object KubernetesArgs { 27 | object Output { 28 | /** 29 | * Represents user input to save generated resources into the directory specified by [[dir]]. 30 | */ 31 | case class SaveToFile(dir: String) extends Output 32 | 33 | /** 34 | * Represents user input to pipe the generated resources into the stream specified by [[out]]. 35 | * The generated resources will be formatted in the format acceptable to `kubectl` command. 36 | */ 37 | case class PipeToStream(out: PrintStream) extends Output 38 | } 39 | 40 | /** 41 | * Base trait which indicates the output required for generated kubernetes resources. 42 | */ 43 | sealed trait Output 44 | 45 | val DefaultNumberOfReplicas: Int = 1 46 | val DefaultImagePullPolicy: PodTemplate.ImagePullPolicy.Value = PodTemplate.ImagePullPolicy.IfNotPresent 47 | 48 | lazy val DefaultAppsApiVersion: Future[String] = kubectl.findApi("apps/v1beta2", "apps/v1beta1") 49 | lazy val DefaultBatchApiVersion: Future[String] = kubectl.findApi("batch/v1", "batch/v1beta1") 50 | lazy val DefaultNamespaceApiVersion: Future[String] = kubectl.findApi("v1") 51 | lazy val DefaultIngressApiVersion: Future[String] = kubectl.findApi("extensions/v1beta1") 52 | lazy val DefaultServiceApiVersion: Future[String] = kubectl.findApi("v1") 53 | 54 | /** 55 | * Convenience method to set the [[KubernetesArgs]] values when parsing the complete user input. 56 | * Refer to [[InputArgs.parser()]] for more details. 57 | */ 58 | def set[T](f: (T, KubernetesArgs) => KubernetesArgs): (T, InputArgs) => InputArgs = { (val1: T, inputArgs: InputArgs) => 59 | GenerateDeploymentArgs 60 | .set { (val2: T, deploymentArgs) => 61 | deploymentArgs.targetRuntimeArgs match { 62 | case Some(v: KubernetesArgs) => 63 | deploymentArgs.copy(targetRuntimeArgs = Some(f(val2, v))) 64 | case _ => deploymentArgs 65 | } 66 | } 67 | .apply(val1, inputArgs) 68 | 69 | } 70 | } 71 | 72 | /** 73 | * Represents user input arguments required to build Kubernetes specific resources. 74 | */ 75 | case class KubernetesArgs( 76 | generateIngress: Boolean = false, 77 | generateNamespaces: Boolean = false, 78 | generatePodControllers: Boolean = false, 79 | generateServices: Boolean = false, 80 | transformIngress: Option[JsonTransformExpression] = None, 81 | transformNamespaces: Option[JsonTransformExpression] = None, 82 | transformPodControllers: Option[JsonTransformExpression] = None, 83 | transformServices: Option[JsonTransformExpression] = None, 84 | namespace: Option[String] = None, 85 | podControllerArgs: PodControllerArgs = PodControllerArgs(), 86 | serviceArgs: ServiceArgs = ServiceArgs(), 87 | ingressArgs: IngressArgs = IngressArgs(), 88 | output: KubernetesArgs.Output = KubernetesArgs.Output.PipeToStream(System.out)) extends TargetRuntimeArgs 89 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/argparse/kubernetes/PodControllerArgs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argparse.kubernetes 18 | 19 | import com.lightbend.rp.reactivecli.argparse.InputArgs 20 | import com.lightbend.rp.reactivecli.runtime.kubernetes.PodTemplate 21 | import scala.collection.immutable.Seq 22 | import scala.concurrent.Future 23 | 24 | object PodControllerArgs { 25 | sealed trait ControllerType { 26 | def arg: String 27 | } 28 | 29 | object ControllerType { 30 | case object Deployment extends ControllerType { 31 | val arg: String = "deployment" 32 | } 33 | 34 | case object Job extends ControllerType { 35 | val arg: String = "job" 36 | } 37 | 38 | val All: Seq[ControllerType] = Seq(Deployment, Job) 39 | val Default: ControllerType = Deployment 40 | } 41 | 42 | /** 43 | * Convenience method to set the [[PodControllerArgs]] values when parsing the complete user input. 44 | * Refer to [[InputArgs.parser()]] for more details. 45 | */ 46 | def set[T](f: (T, PodControllerArgs) => PodControllerArgs): (T, InputArgs) => InputArgs = { (val1: T, inputArgs: InputArgs) => 47 | KubernetesArgs 48 | .set { (val2: T, kubernetesArgs) => 49 | kubernetesArgs.copy( 50 | podControllerArgs = f(val2, kubernetesArgs.podControllerArgs)) 51 | } 52 | .apply(val1, inputArgs) 53 | } 54 | } 55 | 56 | /** 57 | * Represents user input arguments required to build Kubernetes Deployment resource. 58 | */ 59 | case class PodControllerArgs( 60 | appsApiVersion: Future[String] = KubernetesArgs.DefaultAppsApiVersion, 61 | batchApiVersion: Future[String] = KubernetesArgs.DefaultBatchApiVersion, 62 | controllerType: PodControllerArgs.ControllerType = PodControllerArgs.ControllerType.Deployment, 63 | numberOfReplicas: Int = KubernetesArgs.DefaultNumberOfReplicas, 64 | imagePullPolicy: PodTemplate.ImagePullPolicy.Value = KubernetesArgs.DefaultImagePullPolicy, 65 | restartPolicy: PodTemplate.RestartPolicy.Value = PodTemplate.RestartPolicy.Default) 66 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/argparse/kubernetes/ServiceArgs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argparse.kubernetes 18 | 19 | import com.lightbend.rp.reactivecli.argparse.InputArgs 20 | import scala.concurrent.Future 21 | 22 | object ServiceArgs { 23 | /** 24 | * Convenience method to set the [[ServiceArgs]] values when parsing the complete user input. 25 | * Refer to [[InputArgs.parser()]] for more details. 26 | */ 27 | def set[T](f: (T, ServiceArgs) => ServiceArgs): (T, InputArgs) => InputArgs = { (val1: T, inputArgs: InputArgs) => 28 | KubernetesArgs 29 | .set { (val2: T, kubernetesArgs) => 30 | kubernetesArgs.copy( 31 | serviceArgs = f(val2, kubernetesArgs.serviceArgs)) 32 | } 33 | .apply(val1, inputArgs) 34 | } 35 | } 36 | 37 | /** 38 | * Represents user input arguments required to build Kubernetes Service resource. 39 | */ 40 | case class ServiceArgs(apiVersion: Future[String] = KubernetesArgs.DefaultServiceApiVersion, clusterIp: Option[String] = None, loadBalancerIp: Option[String] = None, serviceType: Option[String] = None) 41 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/argparse/marathon/MarathonArgs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argparse.marathon 18 | 19 | import com.lightbend.rp.reactivecli.argparse.{ GenerateDeploymentArgs, InputArgs, TargetRuntimeArgs } 20 | import com.lightbend.rp.reactivecli.json.JsonTransformExpression 21 | import java.io.PrintStream 22 | import scala.collection.immutable.Seq 23 | 24 | object MarathonArgs { 25 | object Output { 26 | /** 27 | * Represents user input to save generated configuration into the file specified by [[file]]. 28 | */ 29 | case class SaveToFile(file: String) extends Output 30 | 31 | /** 32 | * Represents user input to pipe the generated configuration into the stream specified by [[out]]. 33 | * The generated configuration will be formatted in the format acceptable to `dcos marathon group add` command. 34 | */ 35 | case class PipeToStream(out: PrintStream) extends Output 36 | } 37 | 38 | /** 39 | * Base trait which indicates the output required for generated marathon configuration. 40 | */ 41 | sealed trait Output 42 | 43 | /** 44 | * Convenience method to set the [[MarathonArgs]] values when parsing the complete user input. 45 | * Refer to [[InputArgs.parser()]] for more details. 46 | */ 47 | def set[T](f: (T, MarathonArgs) => MarathonArgs): (T, InputArgs) => InputArgs = { (val1: T, inputArgs: InputArgs) => 48 | GenerateDeploymentArgs 49 | .set { (val2: T, deploymentArgs) => 50 | deploymentArgs.targetRuntimeArgs match { 51 | case Some(v: MarathonArgs) => 52 | deploymentArgs.copy(targetRuntimeArgs = Some(f(val2, v))) 53 | case _ => deploymentArgs 54 | } 55 | } 56 | .apply(val1, inputArgs) 57 | 58 | } 59 | } 60 | 61 | /** 62 | * Represents user input arguments required to build marathon specific configuration. 63 | */ 64 | case class MarathonArgs( 65 | instances: Int = 1, 66 | marathonLbHaproxyGroup: String = "external", 67 | marathonLbHaproxyHosts: Seq[String] = Seq.empty, 68 | namespace: Option[String] = None, 69 | output: MarathonArgs.Output = MarathonArgs.Output.PipeToStream(System.out), 70 | registryForcePull: Boolean = false, 71 | transformOutput: Option[JsonTransformExpression] = None) extends TargetRuntimeArgs 72 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/concurrent/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | import scala.concurrent.{ ExecutionContext, Future, Promise } 20 | import scala.util.{ Failure, Success, Try } 21 | 22 | package object concurrent { 23 | implicit val executionContext: ExecutionContext = Platform.executionContext 24 | 25 | def attempt[T](f: Future[T]): Future[Try[T]] = 26 | f 27 | .map(Success.apply) 28 | .recover { case t: Throwable => Failure(t) } 29 | 30 | def optionToFuture[T](option: Option[T], failMsg: String): Future[T] = 31 | option.fold(Future.failed[T](new NoSuchElementException(failMsg)))(Future.successful) 32 | 33 | def wrapFutureOption[T](f: Future[T]): Future[Option[T]] = { 34 | val p = Promise[Option[T]] 35 | 36 | f.onComplete { 37 | case Failure(f) => 38 | p.success(None) 39 | case Success(s) => 40 | p.success(Some(s)) 41 | } 42 | 43 | p.future 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/docker/Config.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import argonaut._ 20 | 21 | import Argonaut._ 22 | 23 | case class Config(config: Config.Cfg) 24 | 25 | object Config { 26 | case class Cfg( 27 | Hostname: Option[String] = None, 28 | ExposedPorts: Option[Map[String, Map[String, String]]] = None, 29 | Cmd: Option[Vector[String]] = None, 30 | Image: Option[String] = None, 31 | User: Option[String] = None, 32 | Labels: Option[Map[String, String]] = None) 33 | 34 | implicit val cfgCodec: CodecJson[Cfg] = CodecJson.derive[Cfg] 35 | implicit val configCodec: CodecJson[Config] = CodecJson.derive[Config] 36 | } 37 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/docker/DockerCredentials.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.files._ 21 | import com.lightbend.rp.reactivecli.process._ 22 | import com.lightbend.rp.reactivecli.concurrent._ 23 | import scala.collection.immutable.Seq 24 | import scala.concurrent.Future 25 | import scala.concurrent.duration._ 26 | import slogging._ 27 | 28 | import Argonaut._ 29 | 30 | /** 31 | * Holds Docker credentials 32 | * @param registry registry credentials are for 33 | * @param credentials Left(base64 encoded Basic Auth string) or Right((username, password)) 34 | */ 35 | case class DockerCredentials(registry: String, credentials: Either[String, (String, String)]) 36 | 37 | /** 38 | * Finds docker credentials in multiple places: 39 | * ~/.lightbend/docker.credentials 40 | * ~/.docker/config.json 41 | * OS-specific storage (OS X Keychan, Windows credential store, etc.) 42 | */ 43 | object DockerCredentials extends LazyLogging { 44 | private val Registry = "registry" 45 | private val Username = "username" 46 | private val Password = "password" 47 | 48 | def get(credsFilePath: Option[String], configFilePath: Option[String]): Future[Seq[DockerCredentials]] = { 49 | // Credential priorities: 50 | // 1. Lightbend credential file 51 | // 2. Docker credential helpers 52 | // 3. Docker config file 53 | val fromCreds = credsFilePath.map(parseCredsFile).getOrElse(Seq.empty) 54 | val futureFromHelpers = dockercred.getCredentials().recover { 55 | case t: Throwable => 56 | logger.debug("Failed to find any Docker credential helpers", t) 57 | Seq.empty 58 | } 59 | val fromConfig = configFilePath.map(parseDockerConfig).getOrElse(Seq.empty) 60 | 61 | def credsToMap(creds: Seq[DockerCredentials]): Map[String, DockerCredentials] = { 62 | creds.map(c => c.registry -> c).toMap 63 | } 64 | 65 | futureFromHelpers.map { fromHelpers => 66 | // Build maps indexed by registry and combine their keys according to priority. 67 | (credsToMap(fromConfig) ++ credsToMap(fromHelpers) ++ credsToMap(fromCreds)) 68 | .values.toVector 69 | } 70 | } 71 | 72 | def parseDockerConfig(configFilePath: String): Seq[DockerCredentials] = 73 | decodeConfig(readFile(configFilePath)) 74 | 75 | def parseCredsFile(credsFilePath: String): Seq[DockerCredentials] = 76 | decodeCreds(readFile(credsFilePath)) 77 | 78 | /** 79 | * Decodes ~/.docker/config.json, where authentication tokens may be stored. 80 | * Example: 81 | * { 82 | * "auths": { 83 | * "https://index.docker.io/v1/": { 84 | * "auth": "0123abcdef=" 85 | * } 86 | * }, 87 | * "HttpHeaders": { 88 | * "User-Agent": "Docker-Client/17.12.0-ce (linux)" 89 | * } 90 | * } 91 | */ 92 | def decodeConfig(content: String): Seq[DockerCredentials] = { 93 | val auths = Parse.parseOption(content).flatMap(_.hcursor.downField("auths").focus) 94 | val fields = auths.flatMap(_.hcursor.fields) 95 | if (auths.isDefined && fields.isDefined) { 96 | fields.get.flatMap { field => 97 | val auth = auths.flatMap(_.hcursor.downField(field).downField("auth").focus) 98 | auth match { 99 | case Some(token) if token.isString => 100 | Some(DockerCredentials(field, Left(token.string.get))) 101 | case _ => None 102 | } 103 | } 104 | } else Seq.empty 105 | } 106 | 107 | /** 108 | * Decodes a Docker credential file. This is a simplistic format with a number of 109 | * key = value pairs (white-space trimmed) separated by "\n" characters. 110 | * 111 | * Recognized keys: registry, username, password 112 | * 113 | * Example file: 114 | * 115 | * registry = lightbend-docker-registry.bintray.io 116 | * username = hello 117 | * password = there 118 | * 119 | * registry = registry.hub.docker.com 120 | * username = foo 121 | * password = bar 122 | * 123 | * yields 124 | * 125 | * Seq( 126 | * DockerCredentials("lightbend-docker-registry.bintray.io", "hello", "there"), 127 | * DockerCredentials("registry.hub.docker.com", "foo", "bar")) 128 | */ 129 | def decodeCreds(content: String): Seq[DockerCredentials] = 130 | lines(content) 131 | .foldLeft(List.empty[DockerCredentials]) { 132 | case (accum, next) => 133 | val modify = 134 | accum.nonEmpty && ( 135 | accum.head.registry.isEmpty || 136 | accum.head.credentials.fold(_ => false, _._1.isEmpty) || 137 | accum.head.credentials.fold(_ => false, _._2.isEmpty)) 138 | 139 | parseLine(next) match { 140 | case (Registry, registry) => 141 | if (modify) 142 | accum.head.copy(registry = registry) :: accum.tail 143 | else 144 | DockerCredentials(registry, Right("" -> "")) :: accum 145 | 146 | case (Username, username) => 147 | if (modify) 148 | accum.head.copy(credentials = accum.head.credentials.fold(Left(_), right => Right(username -> right._2))) :: accum.tail 149 | else 150 | DockerCredentials("", Right("username" -> "")) :: accum 151 | 152 | case (Password, password) => 153 | if (modify) 154 | accum.head.copy(credentials = accum.head.credentials.fold(Left(_), right => Right(right._1 -> password))) :: accum.tail 155 | else 156 | DockerCredentials("", Right("password" -> "")) :: accum 157 | 158 | case _ => 159 | accum 160 | } 161 | } 162 | .reverse 163 | 164 | private[docker] def parseLine(line: String): (String, String) = { 165 | val parts = line.split("=", 2).lift 166 | 167 | parts(0).map(_.trim).getOrElse("") -> parts(1).map(_.trim).getOrElse("") 168 | } 169 | 170 | private[docker] def lines(content: String): Array[String] = 171 | content 172 | .replaceAllLiterally("\r\n", "\n") 173 | .replaceAllLiterally("\r", "") 174 | .split('\n') 175 | } 176 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/docker/DockerEngine.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.concurrent._ 21 | import com.lightbend.rp.reactivecli.files._ 22 | import com.lightbend.rp.reactivecli.http._ 23 | import scala.concurrent.{ ExecutionContext, Future } 24 | import slogging._ 25 | 26 | import Argonaut._ 27 | 28 | object DockerEngine extends LazyLogging { 29 | def applyDockerHostSettings(settings: HttpSettings, env: Map[String, String]): HttpSettings = { 30 | val credentialFiles = for { 31 | certDir <- env.get("DOCKER_CERT_PATH") 32 | keyFile = pathFor(certDir, "key.pem") 33 | certFile = pathFor(certDir, "cert.pem") 34 | caFile = pathFor(certDir, "ca.pem") 35 | 36 | if fileExists(keyFile) && fileExists(certFile) && fileExists(caFile) 37 | } yield (keyFile, certFile, caFile) 38 | 39 | credentialFiles.fold(settings) { v => 40 | val (keyFile, certFile, caFile) = v 41 | settings.copy( 42 | tlsCacertsPath = Some(caFile), 43 | tlsCertPath = Some(certFile), 44 | tlsKeyPath = Some(keyFile)) 45 | } 46 | } 47 | 48 | def getConfigFromDockerHost(http: Http.HttpExchange, env: Map[String, String])(uri: String)(implicit settings: HttpSettings): Future[Option[SocketConfig]] = 49 | env.get("DOCKER_HOST") match { 50 | case None => 51 | Future.successful(None) 52 | 53 | case Some(host) if host.startsWith("tcp://") => 54 | val verify = env.get("DOCKER_TLS_VERIFY").contains("1") 55 | val protocol = if (verify) "https" else "http" 56 | val url = encodeURI(s"$protocol://${host.replaceFirst("tcp://", "")}/images/$uri/json") 57 | 58 | logger.debug("Attempting to pull config from Engine, {}", url) 59 | 60 | wrapFutureOption( 61 | for { 62 | response <- http(HttpRequest(url)) 63 | 64 | _ = logger.debug(s"Received {} from Engine", response.statusCode) 65 | 66 | config <- getDecoded[SocketConfig](response) 67 | } yield config) 68 | } 69 | 70 | private def getDecoded[T](response: HttpResponse)(implicit decode: DecodeJson[T]): Future[T] = { 71 | if (response.statusCode == 200) 72 | response.body.getOrElse("").decodeEither[T].fold( 73 | err => Future.failed(new IllegalArgumentException(s"Decode Failure: $err")), 74 | Future.successful) 75 | else 76 | Future.failed(new IllegalArgumentException(s"Expected code 200, received ${response.statusCode}${response.body.fold("")(": " + _)}")) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/docker/Image.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | sealed trait ImageRef { 20 | def name: String 21 | def value: String 22 | } 23 | 24 | case class ImageDigest(value: String) extends ImageRef { 25 | def name: String = "digest" 26 | } 27 | 28 | case class ImageTag(value: String) extends ImageRef { 29 | def name: String = "tag" 30 | } 31 | 32 | case class Image( 33 | url: String, 34 | namespace: Option[String], 35 | image: String, 36 | ref: ImageRef, 37 | providedUrl: Option[String], 38 | providedNamespace: Option[String], 39 | providedImage: String, 40 | providedRef: Option[ImageRef]) { 41 | def pullScope: String = s"repository:${namespace.map(ns => s"$ns/")}$image:pull" 42 | } 43 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/docker/Manifest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import argonaut._ 20 | 21 | import Argonaut._ 22 | 23 | case class Manifest( 24 | schemaVersion: Int, 25 | mediaType: String, 26 | config: Manifest.Layer, 27 | layers: Vector[Manifest.Layer]) 28 | 29 | object Manifest { 30 | case class Layer(mediaType: String, size: Int, digest: String) 31 | 32 | implicit val layerCodec: CodecJson[Layer] = CodecJson.derive[Layer] 33 | implicit val manifestCodec: CodecJson[Manifest] = CodecJson.derive[Manifest] 34 | } 35 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/docker/SocketConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.docker.{ Config => RegistryConfig } 21 | import Argonaut._ 22 | 23 | case class SocketConfig(Config: SocketConfig.Cfg) { 24 | def registryConfig: RegistryConfig = RegistryConfig(RegistryConfig.Cfg(Labels = Config.Labels)) 25 | } 26 | 27 | object SocketConfig { 28 | case class Cfg(Labels: Option[Map[String, String]] = None) 29 | 30 | implicit val cfgCodec: CodecJson[Cfg] = CodecJson.derive[Cfg] 31 | implicit val configCodec: CodecJson[SocketConfig] = CodecJson.derive[SocketConfig] 32 | } 33 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/docker/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | package object docker { 20 | val DockerAcceptManifestHeader = "application/vnd.docker.distribution.manifest.v2+json" 21 | val DockerDefaultRegistry = "registry.hub.docker.com" 22 | val DockerDefaultLibrary = "library" 23 | val DockerDefaultTag = "latest" 24 | 25 | /** 26 | * Normalizes authentication realms and determines if they match a supplied registry. 27 | */ 28 | def registryAuthNameMatches(registry: String, authRealm: String): Boolean = { 29 | val protocol = "https://" 30 | 31 | val authRealmToTest = 32 | if (authRealm.startsWith(protocol)) 33 | authRealm.drop(protocol.length) 34 | else 35 | authRealm 36 | 37 | registry == authRealmToTest || (registry == DockerDefaultRegistry && authRealmToTest == "index.docker.io/v1/") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/files/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | import scala.concurrent.Future 20 | 21 | package object files { 22 | def deleteFile(path: String): Unit = Platform.deleteFile(path) 23 | 24 | def fileExists(path: String): Boolean = Platform.fileExists(path) 25 | 26 | def mkDirs(path: String): Unit = Platform.mkDirs(path) 27 | 28 | def parentFor(path: String): String = Platform.parentFor(path) 29 | 30 | def pathFor(components: String*): String = Platform.pathFor(components: _*) 31 | 32 | def readFile(path: String): String = Platform.readFile(path) 33 | 34 | def withTempDir[T](f: String => Future[T]): Future[T] = Platform.withTempDir(f) 35 | 36 | def withTempFile[T](f: String => Future[T]): Future[T] = Platform.withTempFile(f) 37 | 38 | def writeFile(path: String, data: String): Unit = Platform.writeFile(path, data) 39 | } 40 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/http.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | import fastparse.all._ 20 | 21 | package object http { 22 | def encodeURI(uri: String): String = Platform.encodeURI(uri) 23 | 24 | def parseAuthHeader(auth: String): Option[Map[String, String]] = { 25 | val ws = P(CharIn(" \t").rep(1)) 26 | val letters = P(CharIn('a' to 'z', 'A' to 'Z') ~ CharsWhile(_ != '=', min = 0)) 27 | val value = P("\"" ~ CharsWhile(_ != '\"', min = 0).! ~ "\"") 28 | val keyval = P(ws.? ~ letters.! ~ "=" ~ ws.? ~ value) 29 | val parser = P(Start ~ keyval.rep(sep = ",") ~ End) 30 | 31 | parser.parse(auth) match { 32 | case Parsed.Success(seq, _) => Some(seq.toMap) 33 | case Parsed.Failure(_, _, _) => None 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/http/Base64Encoder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | import scala.annotation.tailrec 20 | 21 | object Base64Encoder { 22 | private val charSet: Seq[Char] = 23 | ('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ Seq('+', '/') 24 | private val blockLength: Int = 3 25 | private val padding: String = "=" 26 | 27 | /** 28 | * Inspired by https://en.wikibooks.org/wiki/Algorithm_Implementation/Miscellaneous/Base64#Java 29 | */ 30 | def apply(in: String): String = { 31 | val charsToPad = 32 | if (in.length % blockLength == 0) 33 | 0 34 | else 35 | blockLength - (in.length % blockLength) 36 | 37 | val data = in + ("\u0000" * charsToPad) 38 | val rightPad = padding * charsToPad 39 | 40 | @tailrec 41 | def encodeBlock(blockPosition: Int, result: Seq[Char]): Seq[Char] = 42 | if (blockPosition >= data.length) 43 | result 44 | else { 45 | val n = (data.charAt(blockPosition) << 16) + (data.charAt(blockPosition + 1) << 8) + data.charAt(blockPosition + 2) 46 | val encoded: Seq[Char] = Seq( 47 | charSet((n >> 18) & 63), 48 | charSet((n >> 12) & 63), 49 | charSet((n >> 6) & 63), 50 | charSet(n & 63)) 51 | encodeBlock(blockPosition = blockPosition + blockLength, result ++ encoded) 52 | } 53 | 54 | val chars = encodeBlock(blockPosition = 0, Seq.empty) 55 | chars.mkString.substring(0, chars.length - rightPad.length) + rightPad 56 | } 57 | } -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/http/Http.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | import com.lightbend.rp.reactivecli.Platform 20 | import com.lightbend.rp.reactivecli.concurrent._ 21 | import slogging.LazyLogging 22 | 23 | import scala.concurrent.Future 24 | 25 | object Http extends LazyLogging { 26 | type HttpExchange = HttpRequest => Future[HttpResponse] 27 | 28 | case class InfiniteRedirect(visited: List[String]) extends RuntimeException(s"Infinte redirect detected: $visited") 29 | 30 | def http(implicit settings: HttpSettings): HttpExchange = apply 31 | 32 | def apply(request: HttpRequest)(implicit settings: HttpSettings): Future[HttpResponse] = doRequest(request, Nil) 33 | 34 | private def doRequest( 35 | request: HttpRequest, 36 | visitedUrls: List[String])(implicit settings: HttpSettings): Future[HttpResponse] = { 37 | val isFollowRedirect = request.requestFollowRedirects.getOrElse(settings.followRedirect) 38 | 39 | Platform 40 | .httpRequest(request) 41 | .flatMap { response => 42 | if (isFollowRedirect && 43 | response.statusCode >= 300 && 44 | response.statusCode <= 399 && 45 | response.headers.contains("Location")) { 46 | 47 | val location = response.headers("Location") 48 | 49 | if (visitedUrls.contains(location) || visitedUrls.length >= settings.maxRedirects) { 50 | logger.debug("No more redirects allowed") 51 | Future.failed(InfiniteRedirect(visitedUrls)) 52 | } else { 53 | if (location.indexOf("X-Amz-Credential") > -1) { 54 | // S3 based registry doesn't accept authorization header on redirect 55 | logger.debug("Performing S3 redirect") 56 | doRequest( 57 | HttpRequest(location, tlsValidationEnabled = Some(true), requestFollowRedirects = request.requestFollowRedirects), 58 | location :: visitedUrls) 59 | } else { 60 | doRequest( 61 | request.copy(location), location :: visitedUrls) 62 | } 63 | } 64 | } else { 65 | Future.successful(response) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/http/HttpHeaders.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | /** 20 | * Container class for HTTP headers. Headers are case-insensitive. For example, 21 | * `apply("Cache-Control")` will return the same value as `apply("cache-control")`. 22 | * @param headers 23 | */ 24 | case class HttpHeaders(headers: Map[String, String]) { 25 | private val lowerCaseHeaders = 26 | headers 27 | .keys 28 | .map(h => h.toLowerCase -> h) 29 | .toMap 30 | 31 | def apply(name: String): String = headers(lowerCaseHeaders(name.toLowerCase)) 32 | 33 | def contains(name: String): Boolean = 34 | lowerCaseHeaders.contains(name.toLowerCase) 35 | 36 | def header(name: String): Option[String] = 37 | for { 38 | h <- lowerCaseHeaders.get(name.toLowerCase) 39 | v <- headers.get(h) 40 | } yield v 41 | 42 | def updated(name: String, value: String): HttpHeaders = 43 | copy(headers = headers.updated(headerName(name), value)) 44 | 45 | def remove(name: String): HttpHeaders = 46 | copy(headers = headers - headerName(name)) 47 | 48 | private def headerName(name: String): String = 49 | lowerCaseHeaders.getOrElse(name.toLowerCase, name) 50 | } 51 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/http/HttpRequest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | object HttpRequest { 20 | sealed trait Auth 21 | case class BasicAuth(username: String, password: String) extends Auth 22 | case class BearerToken(value: String) extends Auth 23 | case class EncodedBasicAuth(value: String) extends Auth 24 | } 25 | 26 | case class HttpRequest( 27 | requestUrl: String, 28 | requestMethod: String = "GET", 29 | requestHeaders: HttpHeaders = HttpHeaders(Map.empty), 30 | requestBody: Option[String] = None, 31 | auth: Option[HttpRequest.Auth] = None, 32 | requestFollowRedirects: Option[Boolean] = None, 33 | tlsValidationEnabled: Option[Boolean] = None) { 34 | 35 | def disableFollowRedirects: HttpRequest = copy(requestFollowRedirects = Some(false)) 36 | 37 | def enableFollowRedirects: HttpRequest = copy(requestFollowRedirects = Some(true)) 38 | 39 | def disableTlsValidation: HttpRequest = copy(tlsValidationEnabled = Some(false)) 40 | 41 | def enableTlsValidation: HttpRequest = copy(tlsValidationEnabled = Some(true)) 42 | 43 | def get: HttpRequest = copy(requestMethod = "GET") 44 | 45 | def headers(headers: HttpHeaders): HttpRequest = copy(requestHeaders = headers) 46 | 47 | def headersWithAuth: HttpHeaders = 48 | auth.foldLeft(requestHeaders) { 49 | case (hs, HttpRequest.BasicAuth(username, password)) => 50 | hs.updated( 51 | "Authorization", 52 | s"Basic ${Base64Encoder(s"$username:$password")}") 53 | 54 | case (hs, HttpRequest.BearerToken(bearer)) => 55 | hs.updated("Authorization", s"Bearer $bearer") 56 | 57 | case (hs, HttpRequest.EncodedBasicAuth(value)) => 58 | hs.updated("Authorization", s"Basic $value") 59 | } 60 | 61 | def noContent: HttpRequest = copy(requestBody = None) 62 | 63 | def post: HttpRequest = copy(requestMethod = "POST") 64 | 65 | def url(url: String): HttpRequest = copy(requestUrl = url) 66 | 67 | def withContent(body: String): HttpRequest = copy(requestBody = Some(body)) 68 | 69 | def withHeader(name: String, value: String): HttpRequest = copy(requestHeaders = requestHeaders.updated(name, value)) 70 | 71 | def withoutHeader(name: String): HttpRequest = copy(requestHeaders = requestHeaders.remove(name)) 72 | 73 | def withAuth(auth: HttpRequest.Auth): HttpRequest = copy(auth = Some(auth)) 74 | 75 | def withoutAuth: HttpRequest = copy(auth = None) 76 | } 77 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/http/HttpResponse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | case class HttpResponse(statusCode: Long, headers: HttpHeaders, body: Option[String]) 20 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/http/HttpSettings.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | /** 20 | * Common settings for [[Http]]. 21 | * 22 | * @param followRedirect If true, follow redirect up to the number of hops specified by [[maxRedirects]]. 23 | * Else, doesn't follow redirect, i.e. returns the response with `Location` header. 24 | * @param tlsValidationEnabled If true, instructs the underlying `libcurl` to perform TLS validation. 25 | * Else, `libcurl` will set `CURLOPT_SSL_VERIFYPEER` to `0`. 26 | * @param tlsCacertsPath Paths to CA certs used for TLS validation. 27 | * Optional. If specified, this will be supplied to `libcurl` via `CURLOPT_CAINFO` option. 28 | * @param maxRedirects The maximum number of redirects allowed when attempting HTTP request. 29 | * This setting is in place to prevent infinite redirect loop. 30 | */ 31 | case class HttpSettings( 32 | followRedirect: Boolean = true, 33 | tlsValidationEnabled: Boolean = true, 34 | tlsCacertsPath: Option[String] = None, 35 | tlsCertPath: Option[String] = None, 36 | tlsKeyPath: Option[String] = None, 37 | maxRedirects: Int = HttpSettings.DefaultMaxRedirects) 38 | 39 | object HttpSettings { 40 | val DefaultMaxRedirects = 5 41 | 42 | val default = HttpSettings() 43 | } -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/http/SocketRequest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | case class SocketRequest(socket: String, path: String) { 20 | val url: String = s"http://localhost$path" 21 | } -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/json.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.json 18 | 19 | import _root_.argonaut._ 20 | import scala.concurrent.Future 21 | import com.lightbend.rp.reactivecli.process.jq 22 | 23 | final case class JsonTransformExpression(value: String) extends AnyVal 24 | 25 | sealed trait JsonTransform { 26 | def jsonTransform(json: Json): Future[Json] 27 | } 28 | 29 | object JsonTransform { 30 | def noop: JsonTransform = NoJsonTransform 31 | def jq(expr: JsonTransformExpression): JsonTransform = new jqJsonTransform(expr) 32 | } 33 | 34 | case object NoJsonTransform extends JsonTransform { 35 | def jsonTransform(json: Json) = Future.successful(json) 36 | } 37 | 38 | final class jqJsonTransform(expr: JsonTransformExpression) extends JsonTransform { 39 | def jsonTransform(json: Json) = jq.jsonTransform(json, expr) 40 | } 41 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp 18 | 19 | package object reactivecli { 20 | lazy val environment: Map[String, String] = Platform.environment() 21 | 22 | private[rp] def calculateArgs(supplied: Array[String]): Array[String] = Platform.args(supplied) 23 | 24 | private[rp] def start(): Unit = Platform.start() 25 | 26 | private[rp] def stop(): Unit = Platform.stop() 27 | } 28 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/process/docker.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.process 18 | 19 | import scala.concurrent.Future 20 | import com.lightbend.rp.reactivecli.concurrent._ 21 | 22 | import com.lightbend.rp.reactivecli.docker.Config 23 | import argonaut.CodecJson 24 | 25 | object docker { 26 | def inspectImageForConfig(imageName: String): Future[Option[Config]] = 27 | exec("docker", "inspect", imageName).map { 28 | case (_, json) => 29 | import _root_.argonaut.Argonaut._ 30 | json.decodeOption[List[CmdConfig]].flatMap(_.headOption.map(_.registryConfig)) 31 | } 32 | 33 | import com.lightbend.rp.reactivecli.docker.{ Config => RegistryConfig } 34 | 35 | private case class CmdConfig(Config: RegistryConfig.Cfg) { 36 | def registryConfig: RegistryConfig = RegistryConfig(Config) 37 | } 38 | 39 | private object CmdConfig { 40 | implicit val configUpperCodec: CodecJson[CmdConfig] = CodecJson.derive[CmdConfig] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/process/dockercred.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.process 18 | 19 | import java.util.NoSuchElementException 20 | import com.lightbend.rp.reactivecli.concurrent._ 21 | import com.lightbend.rp.reactivecli.docker.DockerCredentials 22 | import com.lightbend.rp.reactivecli.files._ 23 | import scala.collection.immutable.{ Seq, Map } 24 | import scala.concurrent.Future 25 | import slogging._ 26 | import argonaut._ 27 | import Argonaut._ 28 | 29 | object dockercred extends LazyLogging { 30 | private def isAvailable(kind: String): Future[Boolean] = { 31 | exec(s"docker-credential-$kind", "list").map(_._1 == 0) 32 | } 33 | 34 | // Returns a prioritized sequence of available credential helpers 35 | private def chooseHelpers(): Future[Seq[String]] = { 36 | def step(ks: Seq[String]): Future[List[String]] = 37 | if (ks.isEmpty) 38 | Future.successful(List.empty) 39 | else 40 | isAvailable(ks.head).flatMap { 41 | case true => step(ks.tail).map(ks.head :: _) 42 | case false => step(ks.tail) 43 | } 44 | 45 | // Helper sequence here corresponds to priority 46 | step(Seq("gcloud", "osxkeychain", "wincred", "pass", "secretservice")) 47 | } 48 | 49 | private def getJsonField(json: Json, field: String): Option[String] = 50 | json.hcursor.downField(field).focus.flatMap(_.string) 51 | 52 | // Returns seq of pairs server -> username from a single helper 53 | private def list(kind: String): Future[Seq[(String, String)]] = { 54 | for { 55 | (code, output) <- exec(s"docker-credential-$kind", "list") 56 | } yield { 57 | if (code == 0) { 58 | val json = Parse.parseOption(output) 59 | json.flatMap(_.hcursor.fields).get.flatMap(field => { 60 | json.flatMap(getJsonField(_, field)).flatMap(name => Some(field -> name)) 61 | }) 62 | } else { 63 | logger.debug(s"docker-credential-$kind exited with code $code") 64 | Seq.empty 65 | } 66 | } 67 | } 68 | 69 | // Returns seq of tuples (server, username, helper) merged from multiple helpers 70 | private def listAll(ks: Seq[String]): Future[Seq[(String, String, String)]] = { 71 | def step(ks: Seq[String]): Future[Map[String, (String, String)]] = 72 | if (ks.isEmpty) 73 | Future.successful(Map.empty) 74 | else 75 | list(ks.head).flatMap { creds => 76 | step(ks.tail).map(_ ++ creds.map(c => c._1 -> (c._2, ks.head)).toMap) 77 | } 78 | 79 | step(ks).map(m => m.to[Seq].map { cred => 80 | val (server, (username, kind)) = cred 81 | (server, username, kind) 82 | }) 83 | } 84 | 85 | // Returns pair username -> password 86 | private def get(kind: String, server: String): Future[Option[(String, String)]] = { 87 | withTempFile { inputFile => 88 | writeFile(inputFile, server) 89 | for { 90 | (code, output) <- execWithStdinFile(Seq(s"docker-credential-$kind", "get"), Some(inputFile)) 91 | } yield { 92 | if (code == 0) { 93 | val json = Parse.parseOption(output) 94 | val username = json.flatMap(getJsonField(_, "Username")) 95 | val password = json.flatMap(getJsonField(_, "Secret")) 96 | 97 | (username, password) match { 98 | case (Some(username), Some(password)) => Some(username -> password) 99 | case _ => None 100 | } 101 | } else None 102 | } 103 | } 104 | } 105 | 106 | def getCredentials(): Future[Seq[DockerCredentials]] = { 107 | def step(cs: Seq[(String, String, String)]): Future[List[DockerCredentials]] = { 108 | if (cs.isEmpty) Future.successful(List.empty) 109 | else { 110 | val (server, username, kind) = cs.head 111 | get(kind, server).flatMap { 112 | case Some((username, password)) => step(cs.tail).map { seq => 113 | DockerCredentials(server, Right(username -> password)) :: seq 114 | } 115 | case None => step(cs.tail) 116 | } 117 | } 118 | } 119 | 120 | for { 121 | helpers <- chooseHelpers() 122 | creds <- listAll(helpers) 123 | result <- step(creds) 124 | } yield result 125 | } 126 | } -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/process/jq.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.process 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.concurrent._ 21 | import com.lightbend.rp.reactivecli.files._ 22 | import com.lightbend.rp.reactivecli.json.{ JsonTransformExpression, JsonTransform } 23 | import scala.concurrent.Future 24 | import slogging._ 25 | 26 | import Argonaut._ 27 | 28 | object jq extends LazyLogging { 29 | lazy val available: Future[Boolean] = 30 | exec("jq", "--version").map(_._1 == 0) 31 | 32 | def apply(filter: JsonTransformExpression, input: String): Future[String] = 33 | withTempFile { inputFile => 34 | writeFile(inputFile, input) 35 | 36 | for { 37 | (code, output) <- exec("jq", "-M", filter.value, inputFile.toString) 38 | } yield { 39 | if (code != 0) { 40 | logger.error(output) 41 | 42 | throw new RuntimeException(s"jq exited with $code but 0 was expected") 43 | } 44 | 45 | output 46 | } 47 | } 48 | 49 | def jsonTransform(json: Json, expr: JsonTransformExpression) = 50 | apply(expr, json.nospaces) 51 | .map( 52 | _ 53 | .parse 54 | .fold( 55 | error => throw new RuntimeException(s"Unable to parse output from jq: $error"), 56 | identity)) 57 | } 58 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/process/kubectl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.process 18 | 19 | import com.lightbend.rp.reactivecli.concurrent._ 20 | import scala.collection.immutable.Seq 21 | import scala.concurrent.Future 22 | import slogging._ 23 | 24 | object kubectl extends LazyLogging { 25 | lazy val apiVersions: Future[Seq[String]] = 26 | for { 27 | (code, output) <- exec("kubectl", "api-versions", "--request-timeout=1s") 28 | } yield { 29 | if (code == 0) { 30 | output 31 | .replaceAllLiterally("\r\n", "\n") 32 | .split('\n') 33 | .map(_.trim) 34 | .filter(_.nonEmpty) 35 | .toVector 36 | } else { 37 | logger.debug(s"kubectl version exited with $code") 38 | 39 | Seq.empty 40 | } 41 | } 42 | 43 | def findApi(preferred: String, other: String*): Future[String] = { 44 | apiVersions.map { versions => 45 | (preferred +: other) 46 | .find(versions.contains) 47 | .getOrElse(preferred) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/process/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | import scala.concurrent.Future 20 | import slogging._ 21 | 22 | package object process extends LazyLogging { 23 | def exec(args: String*): Future[(Int, String)] = 24 | Platform.processExec(Seq(args: _*)) 25 | 26 | def execWithStdinFile(args: Seq[String], stdinFile: Option[String]): Future[(Int, String)] = 27 | Platform.processExec(args, stdinFile) 28 | } 29 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/GeneratedResource.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime 18 | 19 | import scala.concurrent.Future 20 | 21 | /** 22 | * Base type which represents resource generated by the CLI. 23 | */ 24 | private[reactivecli] trait GeneratedResource[T] { 25 | def resourceType: String 26 | def name: String 27 | def payload: Future[T] 28 | } 29 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/AssignedPort.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import com.lightbend.rp.reactivecli.annotations.Endpoint 20 | 21 | /** 22 | * Represents [[Endpoint]] with its assigned [[port]] number. 23 | */ 24 | case class AssignedPort(endpoint: Endpoint, port: Int) 25 | 26 | object AssignedPort { 27 | /** 28 | * If an endpoint port is undeclared, i.e. `0` it will be assigned a port number starting from this number. 29 | */ 30 | private val AutoPortNumberStart = 10000 31 | 32 | /** 33 | * If an endpoint is undeclared, its port number will be of this value. 34 | */ 35 | private val UndeclaredPortNumber = 0 36 | 37 | /** 38 | * Allocate port number to each endpoint based on: 39 | * - the endpoint's own declared port number, or, 40 | * - if the endpoint point is undeclared, it will be obtained based on the incremented value of 41 | * [[AutoPortNumberStart]]. 42 | */ 43 | def assignPorts(endpoints: Map[String, Endpoint]): Seq[AssignedPort] = 44 | endpoints.values.toList 45 | .foldLeft((AutoPortNumberStart, Seq.empty[AssignedPort])) { (acc, endpoint) => 46 | val (autoPortNumberLast, endpointsAndAssignedPort) = acc 47 | 48 | val (autoPortNumberNext, assignedPort) = 49 | if (endpoint.port == UndeclaredPortNumber) { 50 | autoPortNumberLast + 1 -> autoPortNumberLast 51 | } else { 52 | autoPortNumberLast -> endpoint.port 53 | } 54 | 55 | autoPortNumberNext -> (endpointsAndAssignedPort :+ AssignedPort(endpoint, assignedPort)) 56 | } 57 | ._2 58 | 59 | } 60 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/Deployment.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.annotations._ 21 | import com.lightbend.rp.reactivecli.argparse._ 22 | import com.lightbend.rp.reactivecli.json.{ JsonTransform, JsonTransformExpression } 23 | import scala.collection.immutable.Seq 24 | import scalaz._ 25 | 26 | import Argonaut._ 27 | import PodTemplate._ 28 | import Scalaz._ 29 | 30 | object Deployment { 31 | def restartPolicyValidation(restartPolicy: RestartPolicy.Value): ValidationNel[String, RestartPolicy.Value] = 32 | if (restartPolicy == RestartPolicy.Never || restartPolicy == RestartPolicy.OnFailure) 33 | "Restart policy Never/OnFailure is not valid for Deployment pod controller".failureNel 34 | else 35 | restartPolicy.successNel 36 | 37 | /** 38 | * Builds [[Deployment]] resource. 39 | */ 40 | def generate( 41 | annotations: Annotations, 42 | apiVersion: String, 43 | application: Option[String], 44 | imageName: String, 45 | imagePullPolicy: ImagePullPolicy.Value, 46 | restartPolicy: RestartPolicy.Value, 47 | noOfReplicas: Int, 48 | externalServices: Map[String, Seq[String]], 49 | deploymentType: DeploymentType, 50 | discoveryMethod: DiscoveryMethod, 51 | jsonTransform: JsonTransform, 52 | akkaClusterJoinExisting: Boolean): ValidationNel[String, Deployment] = 53 | 54 | (annotations.applicationValidation(application) 55 | |@| annotations.appNameValidation 56 | |@| annotations.versionValidation 57 | |@| restartPolicyValidation(restartPolicy)) { (applicationArgs, rawAppName, version, restartPolicy) => 58 | val appName = serviceName(rawAppName) 59 | val appNameVersion = serviceName(s"$appName$VersionSeparator$version") 60 | 61 | val serviceResourceName = 62 | deploymentType match { 63 | case CanaryDeploymentType => appName 64 | case BlueGreenDeploymentType => appNameVersion 65 | case RollingDeploymentType => appName 66 | } 67 | 68 | val labels = Map( 69 | "app" -> appName, 70 | "appNameVersion" -> appNameVersion) ++ annotations.akkaClusterBootstrapSystemName.fold(Map( 71 | serviceNameLabel -> serviceResourceName))(system => Map(serviceNameLabel -> system)) 72 | 73 | val podTemplate = 74 | PodTemplate.generate( 75 | annotations, 76 | apiVersion, 77 | application, 78 | imageName, 79 | imagePullPolicy, 80 | noOfReplicas, 81 | RestartPolicy.Always, // The only valid RestartPolicy for Deployment 82 | externalServices, 83 | deploymentType, 84 | discoveryMethod, 85 | akkaClusterJoinExisting, 86 | applicationArgs, 87 | appName, 88 | appNameVersion, 89 | labels) 90 | 91 | val (deploymentName, deploymentMatchLabels) = 92 | deploymentType match { 93 | case CanaryDeploymentType => 94 | (appNameVersion, Json("appNameVersion" -> appNameVersion.asJson)) 95 | 96 | case BlueGreenDeploymentType => 97 | (appNameVersion, Json("appNameVersion" -> appNameVersion.asJson)) 98 | 99 | case RollingDeploymentType => 100 | (appName, Json("app" -> appName.asJson)) 101 | } 102 | 103 | Deployment( 104 | deploymentName, 105 | Json( 106 | "apiVersion" -> apiVersion.asJson, 107 | "kind" -> "Deployment".asJson, 108 | "metadata" -> Json( 109 | "name" -> deploymentName.asJson, 110 | "labels" -> labels.asJson) 111 | .deepmerge( 112 | annotations.namespace.fold(jEmptyObject)(ns => Json("namespace" -> serviceName(ns).asJson))), 113 | "spec" -> Json( 114 | "replicas" -> noOfReplicas.asJson, 115 | "selector" -> Json("matchLabels" -> deploymentMatchLabels), 116 | "template" -> podTemplate.json)), 117 | jsonTransform) 118 | } 119 | } 120 | 121 | /** 122 | * Represents the generated Kubernetes deployment resource. 123 | */ 124 | case class Deployment(name: String, json: Json, jsonTransform: JsonTransform) extends GeneratedKubernetesResource { 125 | val resourceType = "deployment" 126 | } 127 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/GeneratedKubernetesResource.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.json.JsonTransform 21 | import com.lightbend.rp.reactivecli.runtime.GeneratedResource 22 | import scala.concurrent.Future 23 | 24 | /** 25 | * Base type which represents generated Kubernetes resource. 26 | */ 27 | private[reactivecli] trait GeneratedKubernetesResource extends GeneratedResource[Json] { 28 | def json: Json 29 | def jsonTransform: JsonTransform 30 | 31 | def payload: Future[Json] = jsonTransform.jsonTransform(json) 32 | } 33 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/Ingress.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.annotations._ 21 | import com.lightbend.rp.reactivecli.json.{ JsonTransform, JsonTransformExpression } 22 | import com.lightbend.rp.reactivecli.runtime._ 23 | import scala.collection.immutable.Seq 24 | import scalaz._ 25 | import Argonaut._ 26 | import Scalaz._ 27 | 28 | object Ingress { 29 | case class EncodedEndpoint(serviceName: String, servicePort: Int, paths: Seq[String], host: Option[String]) 30 | 31 | def encodeEndpoints( 32 | appName: String, 33 | endpoints: Map[String, Endpoint], 34 | pathAppend: Option[String], 35 | hostsOverride: Option[Seq[String]]): List[EncodedEndpoint] = { 36 | 37 | val ports = AssignedPort.assignPorts(endpoints) 38 | 39 | val httpEndpoints = 40 | endpoints 41 | .collect { 42 | case (_, httpEndpoint: HttpEndpoint) => httpEndpoint 43 | } 44 | .toList 45 | 46 | val append = pathAppend.getOrElse("") 47 | 48 | for { 49 | endpoint <- httpEndpoints 50 | port <- ports.find(_.endpoint == endpoint).toVector 51 | ingress <- endpoint.ingress 52 | host <- hostsOverride.getOrElse(if (ingress.hosts.isEmpty) Seq("") else ingress.hosts) 53 | paths = if (ingress.paths.isEmpty) Seq("") else ingress.paths 54 | } yield { 55 | val pathEntries = 56 | for { 57 | path <- paths 58 | } yield if (path.isEmpty) 59 | "" 60 | else 61 | path + (if (path.endsWith("/") && append.startsWith("/")) append.drop(1) else append) 62 | 63 | EncodedEndpoint(appName, port.port, pathEntries, if (host.isEmpty) None else Some(host)) 64 | } 65 | } 66 | 67 | def renderEndpoints(endpoints: Seq[EncodedEndpoint]): Json = { 68 | case class Path(serviceName: String, servicePort: Int, path: String) { 69 | def depthAndLength: (Int, Int) = pathDepthAndLength(path) 70 | } 71 | 72 | val byHost = 73 | endpoints 74 | .groupBy(_.host) 75 | .toVector 76 | .sortBy(_._1) 77 | .map { 78 | case (host, endpoints) => 79 | val paths = 80 | endpoints 81 | .foldLeft(Seq.empty[Path]) { 82 | case (ac, e) => 83 | ac ++ e.paths.map(p => Path(e.serviceName, e.servicePort, p)) 84 | } 85 | .distinct 86 | .sortBy(_.depthAndLength) 87 | .reverse 88 | 89 | host -> paths 90 | } 91 | 92 | jArray( 93 | byHost.toList.map { 94 | case (host, paths) => 95 | host 96 | .fold(jEmptyObject)(h => jObjectFields("host" -> jString(h))) 97 | .deepmerge( 98 | jObjectFields("http" -> 99 | jObjectFields( 100 | "paths" -> jArray( 101 | paths.toList.map(p => 102 | (if (p.path.nonEmpty) jObjectFields("path" -> jString(p.path)) else jEmptyObject) 103 | .deepmerge( 104 | jObjectFields( 105 | "backend" -> jObjectFields( 106 | "serviceName" -> jString(p.serviceName), 107 | "servicePort" -> jNumber(p.servicePort))))))))) 108 | }) 109 | } 110 | 111 | /** 112 | * Generates the [[Ingress]] resources. 113 | */ 114 | def generate( 115 | annotations: Annotations, 116 | apiVersion: String, 117 | hosts: Option[Seq[String]], 118 | ingressAnnotations: Map[String, String], 119 | jsonTransform: JsonTransform, 120 | name: Option[String], 121 | pathAppend: Option[String], 122 | tlsSecrets: Seq[String]): ValidationNel[String, Option[Ingress]] = { 123 | annotations 124 | .appNameValidation 125 | .map { rawAppName => 126 | val appName = serviceName(rawAppName) 127 | val actualName = name.map(serviceName(_)).getOrElse(appName) 128 | val encodedEndpoints = encodeEndpoints(appName, annotations.endpoints, pathAppend, hosts) 129 | 130 | if (encodedEndpoints.isEmpty) 131 | None 132 | else 133 | Some( 134 | Ingress( 135 | actualName, 136 | encodedEndpoints, 137 | Json( 138 | "apiVersion" -> apiVersion.asJson, 139 | "kind" -> "Ingress".asJson, 140 | "metadata" -> Json( 141 | "name" -> actualName.asJson) 142 | .deepmerge(generateIngressAnnotations(ingressAnnotations)) 143 | .deepmerge(generateNamespaceAnnotation(annotations.namespace)), 144 | "spec" -> Json( 145 | "rules" -> renderEndpoints(encodedEndpoints)).deepmerge( 146 | if (tlsSecrets.isEmpty) jEmptyObject else jObjectFields("tls" -> jArray( 147 | tlsSecrets.toList.map(s => jObjectFields("secretName" -> jString(s))))))), 148 | jsonTransform)) 149 | } 150 | } 151 | 152 | def merge(name: String, a: Ingress, b: Ingress): Ingress = { 153 | val endpoints = a.endpoints ++ b.endpoints 154 | val appName = serviceName(name) 155 | 156 | val merged = a.json.deepmerge(b.json) 157 | val maybeUpdated = -(merged.hcursor --\ "metadata" --\ "name" := jString(appName)) 158 | val updated = 159 | maybeUpdated 160 | .getOrElse(merged) 161 | .deepmerge(jObjectFields("spec" -> jObjectFields("rules" -> renderEndpoints(endpoints)))) 162 | 163 | Ingress(appName, endpoints, updated, b.jsonTransform) 164 | } 165 | 166 | private def generateIngressAnnotations(ingressAnnotations: Map[String, String]): Json = 167 | if (ingressAnnotations.isEmpty) 168 | Json.jEmptyObject 169 | else 170 | Json( 171 | "annotations" -> ingressAnnotations 172 | .map { 173 | case (k, v) => 174 | Json(k -> v.asJson) 175 | } 176 | .reduce(_.deepmerge(_))) 177 | 178 | private def generateNamespaceAnnotation(namespace: Option[String]): Json = 179 | namespace.fold(jEmptyObject)(ns => Json("namespace" -> serviceName(ns).asJson)) 180 | } 181 | 182 | /** 183 | * Represents the generated ingress resource. 184 | */ 185 | case class Ingress(name: String, endpoints: List[Ingress.EncodedEndpoint], json: Json, jsonTransform: JsonTransform) extends GeneratedKubernetesResource { 186 | val resourceType = "ingress" 187 | } 188 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/Job.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.annotations._ 21 | import com.lightbend.rp.reactivecli.argparse._ 22 | import com.lightbend.rp.reactivecli.json.JsonTransform 23 | import scala.collection.immutable.Seq 24 | import scalaz._ 25 | 26 | import Argonaut._ 27 | import PodTemplate._ 28 | import Scalaz._ 29 | 30 | object Job { 31 | def restartPolicyValidation(restartPolicy: RestartPolicy.Value): ValidationNel[String, RestartPolicy.Value] = 32 | if (restartPolicy == RestartPolicy.Always) 33 | "Restart policy Always is not valid for Job pod controller".failureNel 34 | else 35 | restartPolicy.successNel 36 | 37 | /** 38 | * Builds [[Job]] resource. 39 | */ 40 | def generate( 41 | annotations: Annotations, 42 | apiVersion: String, 43 | application: Option[String], 44 | imageName: String, 45 | imagePullPolicy: ImagePullPolicy.Value, 46 | restartPolicy: RestartPolicy.Value, 47 | noOfReplicas: Int, 48 | externalServices: Map[String, Seq[String]], 49 | deploymentType: DeploymentType, 50 | discoveryMethod: DiscoveryMethod, 51 | jsonTransform: JsonTransform, 52 | akkaClusterJoinExisting: Boolean): ValidationNel[String, Job] = 53 | 54 | (annotations.applicationValidation(application) 55 | |@| annotations.appNameValidation 56 | |@| annotations.versionValidation 57 | |@| restartPolicyValidation(restartPolicy)) { (applicationArgs, rawAppName, version, restartPolicy) => 58 | val appName = serviceName(rawAppName) 59 | val appNameVersion = serviceName(s"$appName$VersionSeparator$version") 60 | val serviceResourceName = 61 | deploymentType match { 62 | case CanaryDeploymentType => appName 63 | case BlueGreenDeploymentType => appNameVersion 64 | case RollingDeploymentType => appName 65 | } 66 | 67 | val labels = Map( 68 | "app" -> appName, 69 | "appNameVersion" -> appNameVersion) ++ annotations.akkaClusterBootstrapSystemName.fold(Map( 70 | serviceNameLabel -> serviceResourceName))(system => Map(serviceNameLabel -> system)) 71 | 72 | val podTemplate = 73 | PodTemplate.generate( 74 | annotations, 75 | apiVersion, 76 | application, 77 | imageName, 78 | imagePullPolicy, 79 | noOfReplicas, 80 | if (restartPolicy == RestartPolicy.Default) RestartPolicy.OnFailure else restartPolicy, 81 | externalServices, 82 | deploymentType, 83 | discoveryMethod, 84 | akkaClusterJoinExisting, 85 | applicationArgs, 86 | appName, 87 | appNameVersion, 88 | labels) 89 | 90 | val jobName = 91 | deploymentType match { 92 | case CanaryDeploymentType => appNameVersion 93 | case BlueGreenDeploymentType => appNameVersion 94 | case RollingDeploymentType => appName 95 | } 96 | 97 | Job( 98 | jobName, 99 | Json( 100 | "apiVersion" -> apiVersion.asJson, 101 | "kind" -> jString("Job"), 102 | "metadata" -> Json( 103 | "name" -> jobName.asJson, 104 | "labels" -> labels.asJson) 105 | .deepmerge( 106 | annotations.namespace.fold(jEmptyObject)(ns => Json("namespace" -> serviceName(ns).asJson))), 107 | "spec" -> Json( 108 | "template" -> podTemplate.json)), 109 | jsonTransform) 110 | } 111 | } 112 | 113 | /** 114 | * Represents the generated Kubernetes job resource. 115 | */ 116 | case class Job(name: String, json: Json, jsonTransform: JsonTransform) extends GeneratedKubernetesResource { 117 | val resourceType = "job" 118 | } 119 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/Namespace.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.annotations.Annotations 21 | import com.lightbend.rp.reactivecli.json.{ JsonTransform, JsonTransformExpression } 22 | import scalaz._ 23 | import Argonaut._ 24 | import Scalaz._ 25 | 26 | object Namespace { 27 | /** 28 | * Builds [[Namespace]] resource. 29 | */ 30 | def generate( 31 | annotations: Annotations, 32 | apiVersion: String, 33 | jsonTransform: JsonTransform): ValidationNel[String, Option[Namespace]] = 34 | annotations 35 | .namespace 36 | .map { rawNs => 37 | val ns = serviceName(rawNs) 38 | 39 | Namespace( 40 | ns, 41 | Json( 42 | "apiVersion" -> apiVersion.asJson, 43 | "kind" -> "Namespace".asJson, 44 | "metadata" -> Json( 45 | "name" -> ns.asJson, 46 | "labels" -> Json( 47 | "name" -> ns.asJson))), 48 | jsonTransform) 49 | } 50 | .successNel 51 | } 52 | 53 | case class Namespace(name: String, json: Json, jsonTransform: JsonTransform) extends GeneratedKubernetesResource { 54 | val resourceType = "namespace" 55 | } 56 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/Service.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.annotations._ 21 | import com.lightbend.rp.reactivecli.argparse._ 22 | import com.lightbend.rp.reactivecli.json.JsonTransform 23 | import scalaz._ 24 | 25 | import Argonaut._ 26 | import Scalaz._ 27 | 28 | object Service { 29 | def encodeEndpoint(endpoint: Endpoint, port: AssignedPort): Json = { 30 | val protocol = endpoint match { 31 | case v: HttpEndpoint => "TCP" 32 | case v: TcpEndpoint => "TCP" 33 | case v: UdpEndpoint => "UDP" 34 | } 35 | 36 | Json( 37 | "name" -> serviceName(endpoint.name).asJson, 38 | "port" -> port.port.asJson, 39 | "protocol" -> protocol.asJson, 40 | "targetPort" -> port.port.asJson) 41 | } 42 | 43 | implicit def encodeEndpoints = EncodeJson[Map[String, Endpoint]] { endpoints => 44 | val ports = AssignedPort.assignPorts(endpoints) 45 | val encoded = 46 | for { 47 | (_, endpoint) <- endpoints 48 | port <- ports.find(_.endpoint == endpoint) 49 | } yield encodeEndpoint(endpoint, port) 50 | 51 | encoded.toList.asJson 52 | } 53 | 54 | /** 55 | * Generates the [[Service]] resource. 56 | */ 57 | def generate( 58 | annotations: Annotations, 59 | apiVersion: String, 60 | clusterIp: Option[String], 61 | deploymentType: DeploymentType, 62 | discoveryMethod: DiscoveryMethod, 63 | jsonTransform: JsonTransform, 64 | loadBalancerIp: Option[String], 65 | serviceType: Option[String]): ValidationNel[String, List[Service]] = 66 | (annotations.appNameValidation |@| annotations.versionValidation) { (rawAppName, version) => 67 | // FIXME there's a bit of code duplicate in Deployment 68 | val appName = serviceName(rawAppName) 69 | val internalAppname = appName + "-internal" 70 | val externalAppname = appName + "-external" 71 | val appNameVersion = serviceName(s"$appName${PodTemplate.VersionSeparator}$version") 72 | 73 | val selector = 74 | deploymentType match { 75 | case CanaryDeploymentType => Json("app" -> appName.asJson) 76 | case RollingDeploymentType => Json("app" -> appName.asJson) 77 | case BlueGreenDeploymentType => Json("appNameVersion" -> appNameVersion.asJson) 78 | } 79 | 80 | def svc(endpoints: Map[String, Endpoint]): List[Service] = 81 | if (endpoints.isEmpty) List() 82 | else List(Service( 83 | appName, 84 | Json( 85 | "apiVersion" -> apiVersion.asJson, 86 | "kind" -> "Service".asJson, 87 | "metadata" -> Json( 88 | "labels" -> Json( 89 | "app" -> appName.asJson), 90 | "name" -> appName.asJson) 91 | .deepmerge( 92 | annotations.namespace.fold(jEmptyObject)(ns => Json("namespace" -> serviceName(ns).asJson))), 93 | "spec" -> Json( 94 | "ports" -> endpoints.asJson, 95 | "selector" -> selector) 96 | .deepmerge(clusterIp.fold(jEmptyObject)(cIp => Json("clusterIP" -> jString(cIp)))) 97 | .deepmerge(serviceType.fold(jEmptyObject)(svcType => Json("type" -> jString(svcType)))) 98 | .deepmerge(loadBalancerIp.fold(jEmptyObject)(lbIp => Json("loadBalancerIP" -> jString(lbIp))))), 99 | jsonTransform)) 100 | 101 | // LoadBalancer seems to be required for SRV record to return A record for the pods. 102 | def dummyService(endpoints: Map[String, Endpoint]): Service = Service( 103 | externalAppname, 104 | Json( 105 | "apiVersion" -> apiVersion.asJson, 106 | "kind" -> "Service".asJson, 107 | "metadata" -> Json( 108 | "labels" -> Json( 109 | "app" -> appName.asJson), 110 | "name" -> externalAppname.asJson) 111 | .deepmerge( 112 | annotations.namespace.fold(jEmptyObject)(ns => Json("namespace" -> serviceName(ns).asJson))), 113 | "spec" -> Json( 114 | "ports" -> endpoints.asJson, 115 | "selector" -> selector, 116 | "type" -> "LoadBalancer".asJson)), 117 | jsonTransform) 118 | 119 | def headlessService(endpoints: Map[String, Endpoint]) = Service( 120 | internalAppname, 121 | Json( 122 | "apiVersion" -> apiVersion.asJson, 123 | "kind" -> "Service".asJson, 124 | "metadata" -> Json( 125 | "labels" -> Json( 126 | "app" -> appName.asJson), 127 | "annotations" -> Json( 128 | "service.alpha.kubernetes.io/tolerate-unready-endpoints" -> jString("true")), 129 | "name" -> internalAppname.asJson) 130 | .deepmerge( 131 | annotations.namespace.fold(jEmptyObject)(ns => Json("namespace" -> serviceName(ns).asJson))), 132 | "spec" -> Json( 133 | "ports" -> endpoints.asJson, 134 | "selector" -> selector, 135 | "clusterIP" -> jString("None"), 136 | "publishNotReadyAddresses" -> jTrue)), 137 | jsonTransform) 138 | 139 | if (annotations.endpoints.isEmpty) List() 140 | else if (discoveryMethod == DiscoveryMethod.AkkaDns) 141 | List(headlessService(annotations.headlessEndpoints)) ::: 142 | (serviceType match { 143 | case Some("LoadBalancer") => List() 144 | case _ => 145 | val dummyEndpoints = Map("dummy" -> TcpEndpoint(0, "dummy", 70)) 146 | List(dummyService(dummyEndpoints)) 147 | }) ::: 148 | svc(annotations.publicEndpoints) 149 | else svc(annotations.endpoints) 150 | } 151 | } 152 | 153 | /** 154 | * Represents the generated Kubernetes service resource. 155 | */ 156 | case class Service(name: String, json: Json, jsonTransform: JsonTransform) extends GeneratedKubernetesResource { 157 | val resourceType = "service" 158 | } 159 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/marathon/GeneratedMarathonConfiguration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.marathon 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.json.JsonTransform 21 | import com.lightbend.rp.reactivecli.runtime.GeneratedResource 22 | import scala.concurrent.Future 23 | 24 | case class GeneratedMarathonConfiguration(resourceType: String, name: String, json: Json, jsonTransform: JsonTransform) extends GeneratedResource[Json] { 25 | def payload: Future[Json] = jsonTransform.jsonTransform(json) 26 | } 27 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/marathon/RpEnvironmentVariables.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime 18 | package marathon 19 | 20 | import com.lightbend.rp.reactivecli.annotations._ 21 | import scala.collection.immutable.Seq 22 | 23 | object RpEnvironmentVariables { 24 | /** 25 | * Environment variables in this set will be space-concatenated when the various environment variable 26 | * maps are merged. 27 | */ 28 | private val ConcatLiteralEnvs = Set("RP_JAVA_OPTS") 29 | 30 | /** 31 | * Generates pod environment variables specific for RP applications. 32 | */ 33 | def envs(namespace: Option[String], annotations: Annotations, serviceResourceName: String, noOfReplicas: Int, externalServices: Map[String, Seq[String]], akkaClusterJoinExisting: Boolean): Map[String, String] = 34 | mergeEnvs( 35 | Map("RP_PLATFORM" -> "mesos") ++ 36 | namespace.map(ns => "RP_NAMESPACE" -> ns), 37 | appNameEnvs(annotations.appName), 38 | annotations.version.fold(Map.empty[String, String])(versionEnvs), 39 | appTypeEnvs(annotations.appType, annotations.modules), 40 | configEnvs(annotations.configResource), 41 | Map("RP_JAVA_OPTS" -> playPidDevNull), 42 | endpointEnvs(annotations.endpoints), 43 | akkaClusterEnvs(annotations.modules, annotations.namespace, serviceResourceName, annotations.managementEndpointName.getOrElse(legacyAkkaManagementPortName), noOfReplicas, annotations.akkaClusterBootstrapSystemName, akkaClusterJoinExisting), 44 | externalServicesEnvs(annotations.modules, externalServices)) 45 | 46 | private def appNameEnvs(appName: Option[String]): Map[String, String] = 47 | appName.fold(Map.empty[String, String])(v => Map("RP_APP_NAME" -> v)) 48 | 49 | private def appTypeEnvs(appType: Option[String], modules: Set[String]): Map[String, String] = { 50 | appType 51 | .toVector 52 | .map("RP_APP_TYPE" -> _) ++ ( 53 | if (modules.isEmpty) Seq.empty else Seq("RP_MODULES" -> modules.toVector.sorted.mkString(","))) 54 | }.toMap 55 | 56 | private def akkaClusterEnvs( 57 | modules: Set[String], 58 | namespace: Option[String], 59 | serviceResourceName: String, 60 | managementEndpointName: String, 61 | noOfReplicas: Int, 62 | akkaClusterBootstrapSystemName: Option[String], 63 | akkaClusterJoinExisting: Boolean): Map[String, String] = 64 | if (!modules.contains(Module.AkkaClusterBootstrapping)) 65 | Map.empty 66 | else 67 | Map( 68 | "RP_JAVA_OPTS" -> Seq( 69 | s"-Dakka.management.cluster.bootstrap.contact-point-discovery.discovery-method=marathon-api", 70 | s"-Dakka.management.cluster.bootstrap.contact-point-discovery.port-name=$managementEndpointName", 71 | s"-Dakka.management.cluster.bootstrap.contact-point-discovery.effective-name=$serviceResourceName", 72 | s"-Dakka.management.cluster.bootstrap.contact-point-discovery.required-contact-point-nr=$noOfReplicas", 73 | akkaClusterBootstrapSystemName.fold("-Dakka.discovery.marathon-api.app-label-query=APP_NAME==%s")(systemName => s"-Dakka.discovery.marathon-api.app-label-query=ACTOR_SYSTEM_NAME==$systemName"), 74 | s"${if (akkaClusterJoinExisting) "-Dakka.management.cluster.bootstrap.form-new-cluster=false" else ""}") 75 | .filter(_.nonEmpty) 76 | .mkString(" ")) 77 | 78 | private def configEnvs(config: Option[String]): Map[String, String] = 79 | config 80 | .map(c => Map("RP_JAVA_OPTS" -> s"-Dconfig.resource=$c")) 81 | .getOrElse(Map.empty) 82 | 83 | private def externalServicesEnvs(modules: Set[String], externalServices: Map[String, Seq[String]]): Map[String, String] = 84 | if (!modules.contains(Module.ServiceDiscovery)) 85 | Map.empty 86 | else 87 | Map( 88 | "RP_JAVA_OPTS" -> 89 | externalServices 90 | .flatMap { 91 | case (name, addresses) => 92 | // We allow '/' as that's the convention used: $serviceName/$endpoint 93 | // We allow '_' as it's currently used for Lagom defaults, i.e. "cas_native" 94 | 95 | val arguments = 96 | for { 97 | (address, i) <- addresses.zipWithIndex 98 | } yield s"-Dcom.lightbend.platform-tooling.service-discovery.external-service-addresses.${serviceName(name, Set('/', '_'))}.$i=$address" 99 | 100 | arguments 101 | } 102 | .mkString(" ")) 103 | 104 | private def versionEnvs(version: String): Map[String, String] = 105 | Map( 106 | "RP_APP_VERSION" -> version) 107 | 108 | private def endpointEnvs(endpoints: Map[String, Endpoint]): Map[String, String] = 109 | if (endpoints.isEmpty) 110 | Map( 111 | "RP_ENDPOINTS_COUNT" -> "0") 112 | else 113 | Map( 114 | "RP_ENDPOINTS_COUNT" -> endpoints.size.toString, 115 | "RP_ENDPOINTS" -> 116 | endpoints.values.toList 117 | .sortBy(_.index) 118 | .map(v => envVarName(v.name)) 119 | .mkString(",")) ++ 120 | endpointPortEnvs(endpoints) 121 | 122 | private def endpointPortEnvs(endpoints: Map[String, Endpoint]): Map[String, String] = 123 | endpoints 124 | .flatMap { 125 | case (_, endpoint) => 126 | Seq( 127 | s"RP_ENDPOINT_${envVarName(endpoint.name)}_HOST" -> "$HOST", 128 | s"RP_ENDPOINT_${envVarName(endpoint.name)}_BIND_HOST" -> "0.0.0.0", 129 | s"RP_ENDPOINT_${envVarName(endpoint.name)}_PORT" -> s"$$PORT_${portEnvName(endpoint.name)}", 130 | s"RP_ENDPOINT_${envVarName(endpoint.name)}_BIND_PORT" -> s"$$PORT_${portEnvName(endpoint.name)}", 131 | s"RP_ENDPOINT_${endpoint.index}_HOST" -> "$HOST", 132 | s"RP_ENDPOINT_${endpoint.index}_BIND_HOST" -> "0.0.0.0", 133 | s"RP_ENDPOINT_${endpoint.index}_PORT" -> s"$$PORT_${portEnvName(endpoint.name)}", 134 | s"RP_ENDPOINT_${endpoint.index}_BIND_PORT" -> s"$$PORT_${portEnvName(endpoint.name)}") 135 | } 136 | .toMap 137 | 138 | private def mergeEnvs(envs: Map[String, String]*): Map[String, String] = { 139 | envs.foldLeft(Map.empty[String, String]) { 140 | case (a1, n) => 141 | n.foldLeft(a1) { 142 | case (a2, (key, v)) if ConcatLiteralEnvs.contains(key) => 143 | a2.updated(key, a2.get(key) match { 144 | case Some(ov) => s"$ov $v".trim 145 | case _ => v 146 | }) 147 | 148 | case (a2, (key, value)) => 149 | a2.updated(key, value) 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /cli/shared/src/main/scala/com/lightbend/rp/reactivecli/runtime/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | package object runtime { 20 | private[reactivecli] val AkkaClusterMinimumReplicas = 2 21 | private[reactivecli] val ReadyCheckUrl = "/platform-tooling/ready" 22 | private[reactivecli] val HealthCheckUrl = "/platform-tooling/healthy" 23 | private[reactivecli] val playPidDevNull = "-Dplay.server.pidfile.path=/dev/null" 24 | 25 | def pathDepthAndLength(path: String): (Int, Int) = { 26 | val depth = path.split('/').length 27 | val length = path.length 28 | depth -> length 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/MinVersionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli 18 | 19 | import utest._ 20 | 21 | object MinVersionTest extends TestSuite { 22 | val tests = this{ 23 | "Parse version" - { 24 | assert(Main.parseVersion("0.1.2") == Some((0, 1, 2))) 25 | assert(Main.parseVersion("1.0.0-SNAPSHOT") == Some((1, 0, 0))) 26 | assert(Main.parseVersion("2.4.8.0") == Some((2, 4, 8))) 27 | assert(Main.parseVersion("1.0") == None) 28 | assert(Main.parseVersion("0.x.1") == None) 29 | assert(Main.parseVersion("blah") == None) 30 | } 31 | "Validate given version" - { 32 | assert(!Main.isVersionValid("0.1.2", 2, 0)) 33 | assert(!Main.isVersionValid("blah", 0, 0)) 34 | assert(Main.isVersionValid("0.1.2", 0, 1)) 35 | assert(Main.isVersionValid("0.1.0", 0, 1)) 36 | assert(Main.isVersionValid("1.0.0", 0, 1)) 37 | assert(Main.isVersionValid("0.1.2-SNAPSHOT", 0, 1)) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/argonaut/YamlRendererTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.argonaut 18 | 19 | import argonaut._ 20 | import utest._ 21 | 22 | import Argonaut._ 23 | 24 | object YamlRendererTest extends TestSuite { 25 | import YamlRenderer.render 26 | 27 | val tests = this{ 28 | def equal[A, B](a: A, b: B) = assert(a == b) 29 | 30 | "render" - { 31 | "empty arrays" - equal(render(jEmptyArray), "[]") 32 | 33 | "empty objects" - equal(render(jEmptyObject), "{}") 34 | 35 | "empty string" - equal(render(jEmptyString), "\"\"") 36 | 37 | "zero" - equal(render(jZero), "0") 38 | 39 | "null" - equal(render(jNull), "null") 40 | 41 | "true" - equal(render(jTrue), "true") 42 | 43 | "false" - equal(render(jFalse), "false") 44 | 45 | "true string" - equal(render(jString("true")), "\"true\"") 46 | 47 | "True string" - equal(render(jString("True")), "\"True\"") 48 | 49 | "false string" - equal(render(jString("false")), "\"false\"") 50 | 51 | "False string" - equal(render(jString("False")), "\"False\"") 52 | 53 | "null string" - equal(render(jString("null")), "\"null\"") 54 | 55 | "numeric arrays" - equal( 56 | render(jArrayElements(jNumber(1), jNumber(2), jNumber(3))), 57 | 58 | """|- 1 59 | |- 2 60 | |- 3""".stripMargin) 61 | 62 | "numeric string" - equal(render(jString("12345")), "\"12345\"") 63 | 64 | "simple string" - equal(render(jString("hello world")), "hello world") 65 | 66 | "complex string" - equal(render(jString("hello \"world!\"")), "\"hello \\\"world!\\\"\"") 67 | 68 | "simple object" - equal( 69 | render(jObjectFields("name" -> jString("jason"), "age" -> jNumber(100))), 70 | "name: jason\nage: 100") 71 | 72 | "array of objects" - equal( 73 | render( 74 | jArrayElements( 75 | jObjectFields("name" -> jString("john!"), "age" -> jNumber(100), "present?" -> jFalse), 76 | jObjectFields("name" -> jString("jessica"), "age" -> jNumber(101), "present?" -> jTrue))), 77 | 78 | """|- name: "john!" 79 | | age: 100 80 | | "present?": false 81 | |- name: jessica 82 | | age: 101 83 | | "present?": true""".stripMargin) 84 | 85 | "object containing array of objects" - equal( 86 | render( 87 | jObjectFields("people" -> jArrayElements( 88 | jObjectFields("name" -> jString("john"), "age" -> jNumber(100)), 89 | jObjectFields("name" -> jString("jessica"), "age" -> jNumber(101))))), 90 | 91 | """|people: 92 | | - name: john 93 | | age: 100 94 | | - name: jessica 95 | | age: 101""".stripMargin) 96 | 97 | "nested arrays" - equal( 98 | render( 99 | jArrayElements( 100 | jArrayElements(jNumber(1), jNumber(2), jNumber(3)), 101 | jArrayElements(jNumber(4), jNumber(5), jNumber(6)), 102 | jArrayElements(jNumber(7), jNumber(8), jNumber(9)))), 103 | 104 | """|- - 1 105 | | - 2 106 | | - 3 107 | |- - 4 108 | | - 5 109 | | - 6 110 | |- - 7 111 | | - 8 112 | | - 9""".stripMargin) 113 | 114 | "nested objects" - equal( 115 | render( 116 | jObjectFields("spec" -> jObjectFields( 117 | "template" -> jObjectFields( 118 | "spec" -> jObjectFields( 119 | "one" -> jNumber(1), 120 | "two" -> jNumber(2), 121 | "three" -> jObjectFields( 122 | "a" -> jArrayElements(jFalse), 123 | "b" -> jArrayElements(jTrue))))))), 124 | 125 | """|spec: 126 | | template: 127 | | spec: 128 | | one: 1 129 | | two: 2 130 | | three: 131 | | a: 132 | | - false 133 | | b: 134 | | - true""".stripMargin) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/docker/ConfigTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import argonaut._ 20 | import utest._ 21 | 22 | import Argonaut._ 23 | 24 | object ConfigTest extends TestSuite { 25 | val tests = this{ 26 | "Decode JSON" - { 27 | assert( 28 | """{}"""" 29 | .decodeOption[Config] 30 | .isEmpty) 31 | 32 | assert( 33 | """{ "config": {} }""" 34 | .decodeOption[Config] 35 | .contains(Config(Config.Cfg()))) 36 | 37 | assert( 38 | """|{ 39 | | "config": { 40 | | "Labels": { 41 | | "test.one": "test one!", 42 | | "test.two": "test two!" 43 | | }, 44 | | "Hostname": "test", 45 | | "ExposedPorts": { "80/tcp": {}}, 46 | | "Cmd": ["/bin", "/sh"], 47 | | "Image": "abc123", 48 | | "User": "root" 49 | | } 50 | |}""" 51 | .stripMargin 52 | .decodeOption[Config] 53 | .contains( 54 | Config( 55 | Config.Cfg( 56 | Labels = Some(Map("test.one" -> "test one!", "test.two" -> "test two!")), 57 | Hostname = Some("test"), 58 | ExposedPorts = Some(Map("80/tcp" -> Map.empty)), 59 | Cmd = Some(Vector("/bin", "/sh")), 60 | Image = Some("abc123"), 61 | User = Some("root"))))) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/docker/DockerCredentialsTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | import scala.collection.immutable.Seq 19 | import utest._ 20 | 21 | object DockerCredentialsTest extends TestSuite { 22 | val tests = this{ 23 | "Parse Credentials" - { 24 | val result = DockerCredentials.decodeCreds( 25 | """|registry = lightbend-docker-registry.bintray.io 26 | |username=hello 27 | |password = there 28 | | 29 | |reg=what 30 | | 31 | |registry= registry.hub.docker.com 32 | |password = bar 33 | |username = foo 34 | | 35 | |registry= 1.hub.docker.com 36 | |password = what 37 | |registry = 2.hub.docker.com 38 | |username = ok 39 | |""".stripMargin) 40 | 41 | val expected = Seq( 42 | DockerCredentials("lightbend-docker-registry.bintray.io", Right("hello", "there")), 43 | DockerCredentials("registry.hub.docker.com", Right("foo", "bar")), 44 | DockerCredentials("2.hub.docker.com", Right("ok", "what"))) 45 | 46 | assert(result == expected) 47 | } 48 | "Parse Docker Config" - { 49 | val resultEmpty = DockerCredentials.decodeConfig("""{"auths": {} }""") 50 | val result = DockerCredentials.decodeConfig( 51 | """ 52 | |{ 53 | | "auths": { 54 | | "https://index.docker.io/v1/": { 55 | | "auth": "0123abcdef=" 56 | | }, 57 | | "lightbend-docker-registry.bintray.io": { 58 | | "auth": "xzyw=" 59 | | } 60 | | } 61 | |} 62 | """.stripMargin) 63 | 64 | val expected = Seq( 65 | DockerCredentials("https://index.docker.io/v1/", Left("0123abcdef=")), 66 | DockerCredentials("lightbend-docker-registry.bintray.io", Left("xzyw="))) 67 | 68 | assert(resultEmpty == Seq.empty) 69 | assert(result == expected) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/docker/DockerPackageTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import utest._ 20 | 21 | object DockerPackageTest extends TestSuite { 22 | val tests = this{ 23 | "exact match" - { 24 | assert(registryAuthNameMatches("test.registry.com", "test.registry.com")) 25 | } 26 | 27 | "DockerHub overwrite" - { 28 | assert(registryAuthNameMatches("registry.hub.docker.com", "https://index.docker.io/v1/")) 29 | assert(registryAuthNameMatches("registry.hub.docker.com", "index.docker.io/v1/")) 30 | } 31 | 32 | "https match" - { 33 | assert(registryAuthNameMatches("test.registry.com", "https://test.registry.com")) 34 | } 35 | 36 | "not matching" - { 37 | assert(!registryAuthNameMatches("test.registry.info", "test.registry.com")) 38 | assert(!registryAuthNameMatches("test2.registry.info", "test.registry.info")) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/docker/DockerRegistryTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import scala.util.Success 20 | import utest._ 21 | 22 | object DockerRegistryTest extends TestSuite { 23 | val tests = this{ 24 | "blobUrl" - { 25 | "with https" - { 26 | assert( 27 | DockerRegistry.blobUrl( 28 | Image( 29 | DockerDefaultRegistry, 30 | Some(DockerDefaultLibrary), 31 | "alpine", 32 | ImageTag("3.5"), 33 | None, 34 | None, 35 | "alpine", 36 | Some(ImageTag("3.5"))), "abc123", useHttps = true) == s"https://$DockerDefaultRegistry/v2/$DockerDefaultLibrary/alpine/blobs/abc123") 37 | } 38 | 39 | "with http" - { 40 | assert( 41 | DockerRegistry.blobUrl( 42 | Image( 43 | DockerDefaultRegistry, 44 | Some(DockerDefaultLibrary), 45 | "alpine", 46 | ImageTag("3.5"), 47 | None, 48 | None, 49 | "alpine", 50 | Some(ImageTag("3.5"))), "abc123", useHttps = false) == s"http://$DockerDefaultRegistry/v2/$DockerDefaultLibrary/alpine/blobs/abc123") 51 | } 52 | } 53 | 54 | "manifestUrl" - { 55 | "with https" - { 56 | assert( 57 | DockerRegistry.manifestUrl( 58 | Image( 59 | DockerDefaultRegistry, 60 | Some(DockerDefaultLibrary), 61 | "alpine", 62 | ImageTag("3.5"), 63 | None, 64 | None, 65 | "alpine", 66 | Some(ImageTag("3.5"))), useHttps = true) == s"https://$DockerDefaultRegistry/v2/$DockerDefaultLibrary/alpine/manifests/3.5") 67 | } 68 | 69 | "with http" - { 70 | assert( 71 | DockerRegistry.manifestUrl( 72 | Image( 73 | DockerDefaultRegistry, 74 | Some(DockerDefaultLibrary), 75 | "alpine", 76 | ImageTag("3.5"), 77 | None, 78 | None, 79 | "alpine", 80 | Some(ImageTag("3.5"))), useHttps = false) == s"http://$DockerDefaultRegistry/v2/$DockerDefaultLibrary/alpine/manifests/3.5") 81 | } 82 | } 83 | 84 | "parseImageUri" - { 85 | assert( 86 | DockerRegistry.parseImageUri("alpine") == 87 | Success( 88 | Image( 89 | DockerDefaultRegistry, 90 | Some(DockerDefaultLibrary), 91 | "alpine", 92 | ImageTag("latest"), 93 | None, 94 | None, 95 | "alpine", 96 | None))) 97 | 98 | assert( 99 | DockerRegistry.parseImageUri("alpine:3.5") == 100 | Success( 101 | Image( 102 | DockerDefaultRegistry, 103 | Some(DockerDefaultLibrary), 104 | "alpine", 105 | ImageTag("3.5"), 106 | None, 107 | None, 108 | "alpine", 109 | Some(ImageTag("3.5"))))) 110 | 111 | assert( 112 | DockerRegistry.parseImageUri("my-registry:5000/alpine:3.5") == 113 | Success( 114 | Image( 115 | "my-registry:5000", 116 | None, 117 | "alpine", 118 | ImageTag("3.5"), 119 | Some("my-registry:5000"), 120 | None, 121 | "alpine", 122 | Some(ImageTag("3.5"))))) 123 | 124 | assert( 125 | DockerRegistry.parseImageUri("lightbend-docker-registry.bintray.io/allyourbase:1.0.arebelongtous") == 126 | Success( 127 | Image( 128 | "lightbend-docker-registry.bintray.io", 129 | None, 130 | "allyourbase", 131 | ImageTag("1.0.arebelongtous"), 132 | Some("lightbend-docker-registry.bintray.io"), 133 | None, 134 | "allyourbase", 135 | Some(ImageTag("1.0.arebelongtous"))))) 136 | 137 | assert( 138 | DockerRegistry.parseImageUri("lightbend-docker.registry.bintray.io/conductr/oci-in-docker") == 139 | Success( 140 | Image( 141 | "lightbend-docker.registry.bintray.io", 142 | Some("conductr"), 143 | "oci-in-docker", 144 | ImageTag("latest"), 145 | Some("lightbend-docker.registry.bintray.io"), 146 | Some("conductr"), 147 | "oci-in-docker", 148 | None))) 149 | 150 | assert( 151 | DockerRegistry.parseImageUri("lightbend-docker.registry.bintray.io/conductr/oci-in-docker:0.1") == 152 | Success( 153 | Image( 154 | "lightbend-docker.registry.bintray.io", 155 | Some("conductr"), 156 | "oci-in-docker", 157 | ImageTag("0.1"), 158 | Some("lightbend-docker.registry.bintray.io"), 159 | Some("conductr"), 160 | "oci-in-docker", 161 | Some(ImageTag("0.1"))))) 162 | 163 | assert( 164 | DockerRegistry.parseImageUri("lightbend-docker.registry.bintray.io/conductr/oci-in-docker@sha256:asdfdfasaw123") == 165 | Success( 166 | Image( 167 | "lightbend-docker.registry.bintray.io", 168 | Some("conductr"), 169 | "oci-in-docker", 170 | ImageDigest("sha256:asdfdfasaw123"), 171 | Some("lightbend-docker.registry.bintray.io"), 172 | Some("conductr"), 173 | "oci-in-docker", 174 | Some(ImageDigest("sha256:asdfdfasaw123"))))) 175 | 176 | assert(DockerRegistry.parseImageUri("").isFailure) 177 | assert(DockerRegistry.parseImageUri("test:").isFailure) 178 | assert(DockerRegistry.parseImageUri(":").isFailure) 179 | } 180 | 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/docker/ManifestTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.docker 18 | 19 | import argonaut.Argonaut._ 20 | import argonaut._ 21 | import utest._ 22 | 23 | object ManifestTest extends TestSuite { 24 | val tests = this{ 25 | "Decode JSON" - { 26 | "empty" - assert( 27 | """{}"""" 28 | .decodeOption[Manifest] 29 | .isEmpty) 30 | 31 | "no layers" - assert( 32 | """|{ 33 | | "schemaVersion": 2, 34 | | "mediaType": "test", 35 | | "config": { "mediaType": "test", "size": 8192, "digest": "abc123" }, 36 | | "layers": [] 37 | |}""" 38 | .stripMargin 39 | .decodeOption[Manifest] 40 | .contains(Manifest(2, "test", Manifest.Layer("test", 8192, "abc123"), Vector.empty))) 41 | 42 | assert( 43 | """|{ 44 | | "schemaVersion": 2, 45 | | "mediaType": "test2", 46 | | "config": { "mediaType": "test2", "size": 4096, "digest": "xyz456" }, 47 | | "layers": [ 48 | | { "mediaType": "test3", "size": 1, "digest": "oh" }, 49 | | { "mediaType": "test4", "size": 2, "digest": "rly" } 50 | | ] 51 | |}""" 52 | .stripMargin 53 | .decodeOption[Manifest] 54 | .contains( 55 | Manifest( 56 | 2, 57 | "test2", 58 | Manifest.Layer("test2", 4096, "xyz456"), 59 | Vector(Manifest.Layer("test3", 1, "oh"), Manifest.Layer("test4", 2, "rly"))))) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/http/HttpTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.http 18 | 19 | import utest._ 20 | 21 | // FIXME scala native doesn't seem to like tests in two different projects in the 22 | // FIXME same build so it throws a [error] (cli/nativetest:nativeLinkNIR) java.nio.file.FileSystemAlreadyExistsException 23 | 24 | object HttpTest extends TestSuite { 25 | val tests = this{ 26 | "Encode Strings to base64" - { 27 | val tests = Map( 28 | "abc" -> "YWJj", 29 | "test" -> "dGVzdA==", 30 | "a" -> "YQ==", 31 | "ab" -> "YWI=", 32 | "abc" -> "YWJj", 33 | "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0" -> "WVdKalpHVm1aMmhwYW10c2JXNXZjSEZ5YzNSMWRuZDRlWHBCUWtORVJVWkhTRWxLUzB4TlRrOVFVVkpUVkZWV1YxaFpXakF4TWpNMA==") 34 | 35 | tests.foreach { 36 | case (in, out) => 37 | val encoded = Base64Encoder(in) 38 | 39 | assert(encoded == out) 40 | } 41 | } 42 | 43 | "Parse authentication header" - { 44 | assert(parseAuthHeader("") == Some(Map())) 45 | assert(parseAuthHeader("key=\"val1\"") == Some(Map("key" -> "val1"))) 46 | assert(parseAuthHeader("a=\"val1\",b = \"val2\"") == Some(Map("a" -> "val1", "b " -> "val2"))) 47 | assert(parseAuthHeader(" a=\"\",b = \"val2\"") == Some(Map("a" -> "", "b " -> "val2"))) 48 | assert(parseAuthHeader(" p a=\"1\",b= \"2\"") == Some(Map("p a" -> "1", "b" -> "2"))) 49 | 50 | val data = """Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:dockercloud/hello-world:pull"""" 51 | assert(parseAuthHeader(data) == Some(Map( 52 | "Bearer realm" -> "https://auth.docker.io/token", 53 | "service" -> "registry.docker.io", 54 | "scope" -> "repository:dockercloud/hello-world:pull"))) 55 | 56 | assert(parseAuthHeader("key =") == None) 57 | assert(parseAuthHeader("key = value") == None) 58 | assert(parseAuthHeader(",") == None) 59 | assert(parseAuthHeader("a=\"val1\"b = \"val2\"") == None) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/JobJsonTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import argonaut._ 20 | import com.lightbend.rp.reactivecli.annotations.{ Annotations, LiteralEnvironmentVariable, Secret } 21 | import com.lightbend.rp.reactivecli.argparse.{ CanaryDeploymentType, DiscoveryMethod } 22 | import com.lightbend.rp.reactivecli.json.JsonTransform 23 | import scala.collection.immutable.Seq 24 | import utest._ 25 | 26 | import Argonaut._ 27 | 28 | object JobJsonTest extends TestSuite { 29 | val annotations = Annotations( 30 | namespace = None, 31 | applications = Vector.empty, 32 | appName = Some("friendimpl"), 33 | appType = Some("basic"), 34 | configResource = None, 35 | diskSpace = None, 36 | memory = None, 37 | cpu = None, 38 | endpoints = Map.empty, 39 | managementEndpointName = None, 40 | remotingEndpointName = None, 41 | secrets = Seq.empty, 42 | privileged = false, 43 | environmentVariables = Map.empty, 44 | version = Some("3.2.1-SNAPSHOT"), 45 | modules = Set.empty, 46 | akkaClusterBootstrapSystemName = None) 47 | 48 | val tests = this{ 49 | "works" - { 50 | 51 | val job = 52 | Job.generate( 53 | annotations, 54 | "batch/v1", 55 | None, 56 | "test/testing:1.0.0", 57 | PodTemplate.ImagePullPolicy.Always, 58 | PodTemplate.RestartPolicy.Default, 59 | noOfReplicas = 1, 60 | Map.empty, 61 | CanaryDeploymentType, 62 | DiscoveryMethod.AkkaDns, 63 | JsonTransform.noop, 64 | true) 65 | 66 | val json = job.toOption.get.json 67 | 68 | val expected = jObjectFields( 69 | "apiVersion" -> jString("batch/v1"), 70 | "kind" -> jString("Job"), 71 | "metadata" -> jObjectFields( 72 | "name" -> jString("friendimpl-v3-2-1-snapshot"), 73 | "labels" -> jObjectFields( 74 | "app" -> jString("friendimpl"), 75 | "appNameVersion" -> jString("friendimpl-v3-2-1-snapshot"), 76 | "akka.lightbend.com/service-name" -> jString("friendimpl"))), 77 | "spec" -> jObjectFields( 78 | "template" -> jObjectFields( 79 | "metadata" -> jObjectFields( 80 | "labels" -> jObjectFields( 81 | "appNameVersion" -> jString("friendimpl-v3-2-1-snapshot"), 82 | "akka.lightbend.com/service-name" -> jString("friendimpl"))), 83 | "spec" -> jObjectFields( 84 | "restartPolicy" -> jString("OnFailure"), 85 | "containers" -> jArrayElements( 86 | jObjectFields( 87 | "name" -> jString("friendimpl"), 88 | "image" -> jString("test/testing:1.0.0"), 89 | "ports" -> jEmptyArray, 90 | "imagePullPolicy" -> jString("Always"), 91 | "volumeMounts" -> jEmptyArray, 92 | "env" -> jArrayElements( 93 | jObjectFields("name" -> jString("RP_APP_NAME"), "value" -> jString("friendimpl")), 94 | jObjectFields("name" -> jString("RP_APP_TYPE"), "value" -> jString("basic")), 95 | jObjectFields("name" -> jString("RP_APP_VERSION"), "value" -> jString("3.2.1-SNAPSHOT")), 96 | jObjectFields("name" -> jString("RP_JAVA_OPTS"), "value" -> jString("-Dplay.server.pidfile.path=/dev/null")), 97 | jObjectFields("name" -> jString("RP_KUBERNETES_POD_IP"), "valueFrom" -> jObjectFields("fieldRef" -> jObjectFields("fieldPath" -> jString("status.podIP")))), 98 | jObjectFields("name" -> jString("RP_KUBERNETES_POD_NAME"), "valueFrom" -> jObjectFields("fieldRef" -> jObjectFields("fieldPath" -> jString("metadata.name")))), 99 | jObjectFields("name" -> jString("RP_NAMESPACE"), "valueFrom" -> jObjectFields("fieldRef" -> jObjectFields("fieldPath" -> jString("metadata.namespace")))), 100 | jObjectFields("name" -> jString("RP_PLATFORM"), "value" -> jString("kubernetes"))))), 101 | "volumes" -> jEmptyArray)))) 102 | 103 | assert(json == expected) 104 | } 105 | 106 | "should fail when restart policy is wrong" - { 107 | val job = 108 | Job.generate( 109 | annotations, 110 | "batch/v1", 111 | None, 112 | "test/testing:1.0.0", 113 | PodTemplate.ImagePullPolicy.Always, 114 | PodTemplate.RestartPolicy.Always, 115 | noOfReplicas = 1, 116 | Map.empty, 117 | CanaryDeploymentType, 118 | DiscoveryMethod.AkkaDns, 119 | JsonTransform.noop, 120 | true) 121 | 122 | assert(!job.toOption.isDefined) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /cli/shared/src/test/scala/com/lightbend/rp/reactivecli/runtime/kubernetes/NamespaceJsonTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Lightbend, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lightbend.rp.reactivecli.runtime.kubernetes 18 | 19 | import argonaut._ 20 | import scala.collection.immutable.Seq 21 | import com.lightbend.rp.reactivecli.annotations.Annotations 22 | import com.lightbend.rp.reactivecli.concurrent._ 23 | import com.lightbend.rp.reactivecli.json.{ JsonTransform, JsonTransformExpression } 24 | import com.lightbend.rp.reactivecli.process.jq 25 | import utest._ 26 | 27 | import Argonaut._ 28 | 29 | object NamespaceJsonTest extends TestSuite { 30 | 31 | val tests = this{ 32 | "json serialization" - { 33 | val annotations = Annotations( 34 | namespace = None, 35 | applications = Vector.empty, 36 | appName = None, 37 | appType = None, 38 | configResource = None, 39 | diskSpace = None, 40 | memory = None, 41 | cpu = None, 42 | endpoints = Map.empty, 43 | managementEndpointName = None, 44 | remotingEndpointName = None, 45 | secrets = Seq.empty, 46 | privileged = false, 47 | environmentVariables = Map.empty, 48 | version = None, 49 | modules = Set.empty, 50 | akkaClusterBootstrapSystemName = None) 51 | 52 | "namespace present" - { 53 | val result = Namespace.generate(annotations.copy(namespace = Some("chirper")), "v1", JsonTransform.noop) 54 | 55 | assert(result.isSuccess) 56 | 57 | val expectedJson = 58 | """ 59 | |{ 60 | | "apiVersion": "v1", 61 | | "kind": "Namespace", 62 | | "metadata": { 63 | | "name": "chirper", 64 | | "labels": { 65 | | "name": "chirper" 66 | | } 67 | | } 68 | |} 69 | """.stripMargin.parse.right.get 70 | assert(result.toOption.get.get == Namespace("chirper", expectedJson, JsonTransform.noop)) 71 | } 72 | 73 | "namespace not present" - { 74 | val result = Namespace.generate(annotations.copy(namespace = None), "v1", JsonTransform.noop) 75 | 76 | assert(result.isSuccess) 77 | assert(result.toOption.get.isEmpty) 78 | } 79 | 80 | "jq works" - { 81 | val result = Namespace.generate(annotations.copy(namespace = Some("chirper")), "v1", JsonTransform.jq(JsonTransformExpression(".jq=\"testing\""))) 82 | 83 | assert(result.isSuccess) 84 | 85 | val expectedJson = 86 | """ 87 | |{ 88 | | "apiVersion": "v1", 89 | | "kind": "Namespace", 90 | | "metadata": { 91 | | "name": "chirper", 92 | | "labels": { 93 | | "name": "chirper" 94 | | } 95 | | }, 96 | | "jq": "testing" 97 | |} 98 | """.stripMargin.parse.right.get 99 | 100 | result.toOption.get.get.payload.map { generatedJson => 101 | assert(expectedJson == generatedJson) 102 | } 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/build.sbt: -------------------------------------------------------------------------------- 1 | // https://github.com/akka/akka-management/tree/master/bootstrap-demo/kubernetes-dns 2 | 3 | import Dependencies._ 4 | import scala.sys.process.Process 5 | import scala.util.control.NonFatal 6 | 7 | ThisBuild / version := "0.1.6" 8 | ThisBuild / organization := "com.example" 9 | ThisBuild / scalaVersion := "2.12.7" 10 | 11 | lazy val check = taskKey[Unit]("check") 12 | lazy val generateYaml = taskKey[Unit]("generateYaml") 13 | 14 | lazy val root = (project in file(".")) 15 | .enablePlugins(SbtReactiveAppPlugin) 16 | .settings( 17 | name := "bootstrap-kubernetes-api-demo", 18 | scalacOptions ++= Seq( 19 | "-encoding", 20 | "UTF-8", 21 | "-feature", 22 | "-unchecked", 23 | "-deprecation", 24 | "-Xlint", 25 | "-Yno-adapted-args", 26 | ), 27 | libraryDependencies ++= Seq( 28 | akkaManagement, 29 | akkaClusterHttp, 30 | akkaCluster, 31 | akkaClusterSharding, 32 | akkaClusterTools, 33 | akkaDiscoveryDns, 34 | akkaSlj4j, 35 | logback, 36 | scalaTest 37 | ), 38 | rpEnableAkkaClusterBootstrap := true, 39 | rpAkkaClusterBootstrapSystemName := "hoboken1", 40 | 41 | // run nativeLink in the host build first 42 | generateYaml := { 43 | val s = streams.value 44 | val nm = name.value 45 | val v = version.value 46 | val namespace = sys.env.get("OC_PROJECT").getOrElse("reactivelibtest1") 47 | val rpPath = file(sys.props("reactiveclipath")) / "reactive-cli-out" 48 | val out = Process(s"$rpPath generate-kubernetes-resources --registry-use-local --generate-all $nm:$v --pod-controller-replicas 3").!! 49 | val x = 50 | if (!Deckhand.isOpenShift) 51 | out.replaceAllLiterally("imagePullPolicy: IfNotPresent", "imagePullPolicy: Never") 52 | else out 53 | .replaceAllLiterally("imagePullPolicy: IfNotPresent", "imagePullPolicy: Always") 54 | .replaceAllLiterally("image: \"" + s"$nm:$v" + "\"", s"image: docker-registry-default.centralpark.lightbend.com/$namespace/$nm:$v") 55 | s.log.info("generated YAML: " + x) 56 | IO.write(target.value / "temp.yaml", x) 57 | }, 58 | 59 | // this logic was taken from test.sh 60 | check := { 61 | val s = streams.value 62 | val nm = name.value 63 | val v = version.value 64 | val namespace = sys.env.get("OC_PROJECT").getOrElse("reactivelibtest1") 65 | val kubectl = Deckhand.kubectl(s.log) 66 | val docker = Deckhand.docker(s.log) 67 | val yamlDir = baseDirectory.value / "kubernetes" 68 | 69 | try { 70 | if (!Deckhand.isOpenShift) { 71 | kubectl.tryCreate(s"namespace $namespace") 72 | kubectl.setCurrentNamespace(namespace) 73 | kubectl.apply(Deckhand.mustache(yamlDir / "rbac.mustache"), 74 | Map( 75 | "namespace" -> namespace 76 | )) 77 | } else { 78 | kubectl.command(s"policy add-role-to-user system:image-builder system:serviceaccount:$namespace:default") 79 | kubectl.apply(Deckhand.mustache(yamlDir / "rbac.mustache"), 80 | Map( 81 | "namespace" -> namespace 82 | )) 83 | docker.tag(s"$nm:$v docker-registry-default.centralpark.lightbend.com/$namespace/$nm:$v") 84 | docker.push(s"docker-registry-default.centralpark.lightbend.com/$namespace/$nm") 85 | } 86 | kubectl.apply(target.value / "temp.yaml") 87 | kubectl.waitForPods(3) 88 | kubectl.describe("pods") 89 | kubectl.checkAkkaCluster(3, _.contains(nm)) 90 | } finally { 91 | kubectl.delete(s"services,pods,deployment --all --namespace $namespace") 92 | kubectl.waitForPods(0) 93 | } 94 | } 95 | ) 96 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/kubernetes/rbac.mustache: -------------------------------------------------------------------------------- 1 | --- 2 | ## added this 3 | kind: Role 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | metadata: 6 | name: pod-reader 7 | rules: 8 | - apiGroups: [""] # "" indicates the core API group 9 | resources: ["pods"] 10 | verbs: ["get", "watch", "list"] 11 | --- 12 | ## added this 13 | kind: RoleBinding 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | metadata: 16 | name: read-pods 17 | subjects: 18 | - kind: User 19 | name: system:serviceaccount:{{namespace}}:default 20 | roleRef: 21 | kind: Role 22 | name: pod-reader 23 | apiGroup: rbac.authorization.k8s.io 24 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | val akkaVersion = "2.5.20" 5 | val akkaManagementVersion = "0.20.0" 6 | 7 | val akkaActor = "com.typesafe.akka" %% "akka-actor" % akkaVersion 8 | val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % akkaVersion 9 | val akkaClusterSharding = "com.typesafe.akka" %% "akka-cluster-sharding" % akkaVersion 10 | val akkaClusterTools = "com.typesafe.akka" %% "akka-cluster-tools" % akkaVersion 11 | val akkaSlj4j = "com.typesafe.akka" %% "akka-slf4j" % akkaVersion 12 | 13 | val akkaManagement = "com.lightbend.akka.management" %% "akka-management" % akkaManagementVersion 14 | val akkaBootstrap = "com.lightbend.akka.management" %% "akka-management-cluster-bootstrap" % akkaManagementVersion 15 | val akkaDiscoveryDns = "com.lightbend.akka.discovery" %% "akka-discovery-dns" % akkaManagementVersion 16 | val akkaClusterHttp = "com.lightbend.akka.management" %% "akka-management-cluster-http" % akkaManagementVersion 17 | 18 | val logback = "ch.qos.logback" % "logback-classic" % "1.2.3" 19 | 20 | val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5" % Test 21 | } 22 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.7 2 | 3 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lightbend.rp" % "sbt-reactive-app" % "1.7.0") 2 | addSbtPlugin("com.lightbend.rp" % "sbt-deckhand" % "0.1.0") 3 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/src/main/scala/foo/ClusterApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2018 Lightbend Inc. 3 | */ 4 | 5 | package foo 6 | 7 | import akka.actor.{ Actor, ActorLogging, ActorSystem, PoisonPill, Props } 8 | import akka.cluster.ClusterEvent.ClusterDomainEvent 9 | import akka.cluster.singleton.{ ClusterSingletonManager, ClusterSingletonManagerSettings } 10 | import akka.cluster.{ Cluster, ClusterEvent } 11 | import akka.http.scaladsl.Http 12 | import akka.http.scaladsl.model._ 13 | import akka.http.scaladsl.server.Directives._ 14 | import akka.stream.ActorMaterializer 15 | 16 | object ClusterApp { 17 | 18 | def main(args: Array[String]): Unit = { 19 | 20 | implicit val system = ActorSystem() 21 | implicit val materializer = ActorMaterializer() 22 | implicit val executionContext = system.dispatcher 23 | 24 | val cluster = Cluster(system) 25 | system.log.info("Starting Akka Management") 26 | system.log.info("something2") 27 | // AkkaManagement(system).start() 28 | // ClusterBootstrap(system).start() 29 | 30 | system.actorOf( 31 | ClusterSingletonManager.props( 32 | Props[NoisySingleton], 33 | PoisonPill, 34 | ClusterSingletonManagerSettings(system))) 35 | Cluster(system).subscribe( 36 | system.actorOf(Props[ClusterWatcher]), 37 | ClusterEvent.InitialStateAsEvents, 38 | classOf[ClusterDomainEvent]) 39 | 40 | // add real app routes here 41 | val routes = 42 | path("hello") { 43 | get { 44 | complete( 45 | HttpEntity(ContentTypes.`text/html(UTF-8)`, "

Hello

")) 46 | } 47 | } 48 | 49 | Http().bindAndHandle(routes, "0.0.0.0", 8080) 50 | 51 | system.log.info( 52 | s"Server online at http://localhost:8080/\nPress RETURN to stop...") 53 | 54 | cluster.registerOnMemberUp(() => { 55 | system.log.info("Cluster member is up!") 56 | }) 57 | } 58 | 59 | class ClusterWatcher extends Actor with ActorLogging { 60 | val cluster = Cluster(context.system) 61 | 62 | override def receive = { 63 | case msg ⇒ log.info(s"Cluster ${cluster.selfAddress} >>> " + msg) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/src/main/scala/foo/NoisySingleton.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2018 Lightbend Inc. 3 | */ 4 | 5 | package foo 6 | 7 | import akka.actor.Actor 8 | import akka.actor.ActorLogging 9 | 10 | class NoisySingleton extends Actor with ActorLogging { 11 | 12 | override def preStart(): Unit = 13 | log.info("Noisy singleton started") 14 | 15 | override def postStop(): Unit = 16 | log.info("Noisy singleton stopped") 17 | 18 | override def receive: Receive = { 19 | case msg => log.info("Msg: {}", msg) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-api/test: -------------------------------------------------------------------------------- 1 | > Docker/publishLocal 2 | > generateYaml 3 | $exists target/temp.yaml 4 | > check 5 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-dns/build.sbt: -------------------------------------------------------------------------------- 1 | // https://github.com/akka/akka-management/tree/master/bootstrap-demo/kubernetes-dns 2 | 3 | import Dependencies._ 4 | import scala.sys.process.Process 5 | import scala.util.control.NonFatal 6 | 7 | ThisBuild / version := "0.1.6" 8 | ThisBuild / organization := "com.example" 9 | ThisBuild / scalaVersion := "2.12.7" 10 | 11 | lazy val check = taskKey[Unit]("check") 12 | lazy val generateYaml = taskKey[Unit]("generateYaml") 13 | 14 | lazy val root = (project in file(".")) 15 | .enablePlugins(SbtReactiveAppPlugin) 16 | .settings( 17 | name := "bootstrap-kubernetes-dns-demo", 18 | scalacOptions ++= Seq( 19 | "-encoding", 20 | "UTF-8", 21 | "-feature", 22 | "-unchecked", 23 | "-deprecation", 24 | "-Xlint", 25 | "-Yno-adapted-args", 26 | ), 27 | libraryDependencies ++= Seq( 28 | akkaManagement, 29 | akkaClusterHttp, 30 | akkaCluster, 31 | akkaClusterSharding, 32 | akkaClusterTools, 33 | akkaDiscoveryDns, 34 | akkaSlj4j, 35 | logback, 36 | scalaTest 37 | ), 38 | rpEnableAkkaClusterBootstrap := true, 39 | 40 | // run nativeLink in the host build first 41 | generateYaml := { 42 | val s = streams.value 43 | val nm = name.value 44 | val v = version.value 45 | val namespace = sys.env.get("OC_PROJECT").getOrElse("reactivelibtest1") 46 | val rpPath = file(sys.props("reactiveclipath")) / "reactive-cli-out" 47 | val out = Process(s"$rpPath generate-kubernetes-resources --registry-use-local --generate-all --discovery-method akka-dns $nm:$v --pod-controller-replicas 3 --stacktrace").!! 48 | val x = 49 | if (!Deckhand.isOpenShift) 50 | out.replaceAllLiterally("imagePullPolicy: IfNotPresent", "imagePullPolicy: Never") 51 | else out 52 | .replaceAllLiterally("imagePullPolicy: IfNotPresent", "imagePullPolicy: Always") 53 | .replaceAllLiterally("image: \"" + s"$nm:$v" + "\"", s"image: docker-registry-default.centralpark.lightbend.com/$namespace/$nm:$v") 54 | s.log.info("generated YAML: " + x) 55 | IO.write(target.value / "temp.yaml", x) 56 | }, 57 | 58 | // this logic was taken from test.sh 59 | check := { 60 | val s = streams.value 61 | val nm = name.value 62 | val v = version.value 63 | val namespace = sys.env.get("OC_PROJECT").getOrElse("reactivelibtest1") 64 | val kubectl = Deckhand.kubectl(s.log) 65 | val docker = Deckhand.docker(s.log) 66 | 67 | try { 68 | try { 69 | if (!Deckhand.isOpenShift) { 70 | kubectl.tryCreate(s"namespace $namespace") 71 | kubectl.setCurrentNamespace(namespace) 72 | } else { 73 | kubectl.command(s"policy add-role-to-user system:image-builder system:serviceaccount:$namespace:default") 74 | docker.tag(s"$nm:$v docker-registry-default.centralpark.lightbend.com/$namespace/$nm:$v") 75 | docker.push(s"docker-registry-default.centralpark.lightbend.com/$namespace/$nm") 76 | } 77 | kubectl.apply(target.value / "temp.yaml") 78 | kubectl.waitForPods(3) 79 | kubectl.describe("pods") 80 | kubectl.checkAkkaCluster(3, _.contains(nm)) 81 | } catch { 82 | case e => kubectl.tryLogs() 83 | } 84 | } finally { 85 | kubectl.delete(s"services,pods,deployment --all --namespace $namespace") 86 | kubectl.waitForPods(0) 87 | } 88 | } 89 | ) 90 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-dns/project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | val akkaVersion = "2.5.20" 5 | val akkaManagementVersion = "0.20.0" 6 | 7 | val akkaActor = "com.typesafe.akka" %% "akka-actor" % akkaVersion 8 | val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % akkaVersion 9 | val akkaClusterSharding = "com.typesafe.akka" %% "akka-cluster-sharding" % akkaVersion 10 | val akkaClusterTools = "com.typesafe.akka" %% "akka-cluster-tools" % akkaVersion 11 | val akkaSlj4j = "com.typesafe.akka" %% "akka-slf4j" % akkaVersion 12 | 13 | val akkaManagement = "com.lightbend.akka.management" %% "akka-management" % akkaManagementVersion 14 | val akkaBootstrap = "com.lightbend.akka.management" %% "akka-management-cluster-bootstrap" % akkaManagementVersion 15 | val akkaDiscoveryDns = "com.lightbend.akka.discovery" %% "akka-discovery-dns" % akkaManagementVersion 16 | val akkaClusterHttp = "com.lightbend.akka.management" %% "akka-management-cluster-http" % akkaManagementVersion 17 | 18 | val logback = "ch.qos.logback" % "logback-classic" % "1.2.3" 19 | 20 | val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5" % Test 21 | } 22 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-dns/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.7 2 | 3 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-dns/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lightbend.rp" % "sbt-reactive-app" % "1.7.0") 2 | addSbtPlugin("com.lightbend.rp" % "sbt-deckhand" % "0.1.0") 3 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-dns/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } 4 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-dns/src/main/scala/foo/ClusterApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2018 Lightbend Inc. 3 | */ 4 | 5 | package foo 6 | 7 | import akka.actor.{ Actor, ActorLogging, ActorSystem, PoisonPill, Props } 8 | import akka.cluster.ClusterEvent.ClusterDomainEvent 9 | import akka.cluster.singleton.{ ClusterSingletonManager, ClusterSingletonManagerSettings } 10 | import akka.cluster.{ Cluster, ClusterEvent } 11 | import akka.http.scaladsl.Http 12 | import akka.http.scaladsl.model._ 13 | import akka.http.scaladsl.server.Directives._ 14 | import akka.stream.ActorMaterializer 15 | 16 | object ClusterApp { 17 | 18 | def main(args: Array[String]): Unit = { 19 | 20 | implicit val system = ActorSystem() 21 | implicit val materializer = ActorMaterializer() 22 | implicit val executionContext = system.dispatcher 23 | 24 | val cluster = Cluster(system) 25 | system.log.info("Starting Akka Management") 26 | system.log.info("something2") 27 | // AkkaManagement(system).start() 28 | // ClusterBootstrap(system).start() 29 | 30 | system.actorOf( 31 | ClusterSingletonManager.props( 32 | Props[NoisySingleton], 33 | PoisonPill, 34 | ClusterSingletonManagerSettings(system))) 35 | Cluster(system).subscribe( 36 | system.actorOf(Props[ClusterWatcher]), 37 | ClusterEvent.InitialStateAsEvents, 38 | classOf[ClusterDomainEvent]) 39 | 40 | // add real app routes here 41 | val routes = 42 | path("hello") { 43 | get { 44 | complete( 45 | HttpEntity(ContentTypes.`text/html(UTF-8)`, "

Hello

")) 46 | } 47 | } 48 | 49 | Http().bindAndHandle(routes, "0.0.0.0", 8080) 50 | 51 | system.log.info( 52 | s"Server online at http://localhost:8080/\nPress RETURN to stop...") 53 | 54 | cluster.registerOnMemberUp(() => { 55 | system.log.info("Cluster member is up!") 56 | }) 57 | } 58 | 59 | class ClusterWatcher extends Actor with ActorLogging { 60 | val cluster = Cluster(context.system) 61 | 62 | override def receive = { 63 | case msg ⇒ log.info(s"Cluster ${cluster.selfAddress} >>> " + msg) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-dns/src/main/scala/foo/NoisySingleton.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2018 Lightbend Inc. 3 | */ 4 | 5 | package foo 6 | 7 | import akka.actor.Actor 8 | import akka.actor.ActorLogging 9 | 10 | class NoisySingleton extends Actor with ActorLogging { 11 | 12 | override def preStart(): Unit = 13 | log.info("Noisy singleton started") 14 | 15 | override def postStop(): Unit = 16 | log.info("Noisy singleton stopped") 17 | 18 | override def receive: Receive = { 19 | case msg => log.info("Msg: {}", msg) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integration-test/src/sbt-test/bootstrap-demo/kubernetes-dns/test: -------------------------------------------------------------------------------- 1 | > Docker/publishLocal 2 | > generateYaml 3 | $exists target/temp.yaml 4 | > check 5 | -------------------------------------------------------------------------------- /project/AdditionalIO.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import scala.sys.process._ 3 | 4 | object AdditionalIO { 5 | def setExecutable(file: File): Unit = 6 | assert(file.setExecutable(true), s"Marking $file executable failed") 7 | 8 | def runProcess(args: String*): Unit = { 9 | val code = args.! 10 | assert(code == 0, s"Executing $args yielded $code, expected 0") 11 | } 12 | 13 | def runProcessCwd(cwd: File, args: String*): Unit = { 14 | val code = Process(args, cwd).! 15 | assert(code == 0, s"Executing $args yielded $code, expected 0") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /project/BintrayExt.scala: -------------------------------------------------------------------------------- 1 | package bintray 2 | 3 | import dispatch._, Defaults._ 4 | import sbt.{ File, Logger } 5 | import scala.concurrent.Await 6 | import scala.concurrent.duration._ 7 | 8 | object RpmBuildTarget { 9 | def normalizeVersion(version: String): String = 10 | version.replaceAll("-", ".") 11 | } 12 | 13 | /** 14 | * sbt-bintray doesn't have implementations for rpm and deb uploading so 15 | * this implements the functionality in the most straight-forward way. 16 | * 17 | * We still use the plugin so that we can read credentials so from a user's 18 | * perspective (for publishing) it behaves in a similar manner. 19 | */ 20 | object BintrayExt { 21 | def publishDeb(file: File, distributions: Seq[String], components: String, architecture: String, version: String, bintrayCredentialsFile: File, log: Logger): Unit = { 22 | val urlString = 23 | s"https://api.bintray.com/content/lightbend/deb/reactive-cli/$version/${file.getName}" 24 | 25 | val request = withAuth(Bintray.ensuredCredentials(bintrayCredentialsFile, log))( 26 | url(urlString) 27 | .addHeader("X-Bintray-Debian-Distribution", distributions.mkString(",")) 28 | .addHeader("X-Bintray-Debian-Component", components) 29 | .addHeader("X-Bintray-Debian-Architecture", architecture) <<< file) 30 | 31 | log.info(s"Uploading ${file.getName} to $urlString") 32 | 33 | val response = Await.result(Http(request), Duration.Inf) 34 | 35 | val responseText = s"[${response.getStatusCode} ${response.getStatusText}] ${response.getResponseBody}" 36 | 37 | if (response.getStatusCode >= 200 && response.getStatusCode <= 299) 38 | log.info(responseText) 39 | else 40 | sys.error(responseText) 41 | } 42 | 43 | def publishRpm(file: File, version: String, bintrayCredentialsFile: File, log: Logger): Unit = { 44 | val urlString = 45 | s"https://api.bintray.com/content/lightbend/rpm/reactive-cli/${RpmBuildTarget.normalizeVersion(version)}/${file.getName}" 46 | 47 | val request = 48 | withAuth(Bintray.ensuredCredentials(bintrayCredentialsFile, log))(url(urlString) <<< file) 49 | 50 | log.info(s"Uploading ${file.getName} to $urlString") 51 | 52 | val response = Await.result(Http(request), Duration.Inf) 53 | 54 | val responseText = s"[${response.getStatusCode} ${response.getStatusText}] ${response.getResponseBody}" 55 | 56 | if (response.getStatusCode >= 200 && response.getStatusCode <= 299) 57 | log.info(responseText) 58 | else 59 | sys.error(responseText) 60 | } 61 | 62 | def publishTarGz(file: File, version: String, bintrayCredentialsFile: File, log: Logger): Unit = { 63 | val urlString = 64 | s"https://api.bintray.com/content/lightbend/generic/reactive-cli/$version/${file.getName}" 65 | 66 | val request = 67 | withAuth(Bintray.ensuredCredentials(bintrayCredentialsFile, log))(url(urlString) <<< file) 68 | 69 | log.info(s"Uploading ${file.getName} to $urlString") 70 | 71 | val response = Await.result(Http(request), Duration.Inf) 72 | 73 | val responseText = s"[${response.getStatusCode} ${response.getStatusText}] ${response.getResponseBody}" 74 | 75 | if (response.getStatusCode >= 200 && response.getStatusCode <= 299) 76 | log.info(responseText) 77 | else 78 | sys.error(responseText) 79 | } 80 | 81 | private def withAuth(credentials: Option[BintrayCredentials])(request: Req) = 82 | credentials.fold(request)(c => request.as_!(c.user, c.password)) 83 | } -------------------------------------------------------------------------------- /project/Properties.scala: -------------------------------------------------------------------------------- 1 | object Properties { 2 | val dynamicLinker = Option(System.getProperty("build.dynamicLinker")) 3 | val memory = Option(System.getProperty("build.mx")).getOrElse("2048") 4 | val nativeMode = System.getProperty("build.nativeMode", "debug") 5 | } 6 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.2.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.6") 2 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "3.0.1") 3 | addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.1") 4 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0") 5 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "0.6.0") 6 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.23") 7 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.3.7") 8 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.0") 9 | -------------------------------------------------------------------------------- /script/install-minikube.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -exu 4 | 5 | # Make root mounted as rshared to fix kube-dns issues. 6 | sudo mount --make-rshared / 7 | # Download kubectl, which is a requirement for using minikube. 8 | curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.9.0/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/ 9 | 10 | # Download minikube. 11 | curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/ 12 | sudo minikube start --vm-driver=none --kubernetes-version=v1.10.0 13 | # Fix the kubectl context, as it's often stale. 14 | minikube update-context 15 | # Wait for Kubernetes to be up and ready. 16 | JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1; done 17 | # kube-addon-manager is responsible for managing other kubernetes components, such as kube-dns, dashboard, storage-provisioner.. 18 | JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl -n kube-system get pods -lcomponent=kube-addon-manager -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1;echo "waiting for kube-addon-manager to be available"; kubectl get pods --all-namespaces; done 19 | # Wait for kube-dns to be ready. 20 | JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl -n kube-system get pods -lk8s-app=kube-dns -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1;echo "waiting for kube-dns to be available"; kubectl get pods --all-namespaces; done 21 | -------------------------------------------------------------------------------- /script/install-oc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -exu 4 | 5 | docker --version 6 | curl -L https://github.com/openshift/origin/releases/download/v3.10.0/openshift-origin-client-tools-v3.10.0-dd10d17-linux-64bit.tar.gz | tar xvz --strip 1 && chmod +x oc && sudo cp oc /usr/local/bin/ && rm oc 7 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "1.7.2-SNAPSHOT" 2 | --------------------------------------------------------------------------------