Google Cloud Platform with Gradle Kotlin DSL
I was trying to deploy a Spring application to App Engine as per the documentation and realized that while the GCP documentation covers the Groovy based DSL for Gradle it doesn't have any official documentation for the newer Kotlin DSL.
Bits and pieces were available online but it took some time to piece it all together. I'm sharing it her in hopes it can save others from having to figure it out again. For those that just want to grab the Kotlin DSL version is here:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.google.cloud.tools.gradle.appengine.standard.AppEngineStandardExtension
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath("com.google.cloud.tools:appengine-gradle-plugin:2.4.1")
}
}
plugins {
war
kotlin("jvm") version "1.4.21"
kotlin("plugin.spring") version "1.4.21"
kotlin("plugin.jpa") version "1.4.21"
}
apply(plugin = "com.google.cloud.tools.appengine")
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
maven {
url = uri("https://oss.sonatype.org/content/repositories/snapshots")
}
mavenCentral()
jcenter()
}
extra["springCloudGcpVersion"] = "2.0.0"
extra["springCloudVersion"] = "2020.0.0"
dependencies {
implementation("com.google.appengine:appengine-api-1.0-sdk:+") // Latest App Engine Api's
providedCompile("javax.servlet:javax.servlet-api:4.0.1")
implementation("jstl:jstl:1.2")
// Add your dependencies here.
// Testing
testImplementation("junit:junit:4.12")
testImplementation("com.google.truth:truth:1.1")
testImplementation("org.mockito:mockito-all:1.10.19")
testImplementation("com.google.appengine:appengine-testing:+")
testImplementation("com.google.appengine:appengine-api-stubs:+")
testImplementation("com.google.appengine:appengine-tools-sdk:+")
}
dependencyManagement {
imports {
mavenBom("com.google.cloud:spring-cloud-gcp-dependencies:${property("springCloudGcpVersion")}")
}
}
tasks.withType<com.google.cloud.tools.gradle.appengine.core.DeployTask> {
dependsOn("test")
}
tasks.withType<com.google.cloud.tools.gradle.appengine.standard.StageStandardTask> {
dependsOn("test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks {
test {
useJUnitPlatform()
testLogging.showStandardStreams = true
beforeTest(closureOf<TestDescriptor> {
logger.lifecycle("test: $this Running")
})
onOutput(KotlinClosure2<TestDescriptor,TestOutputEvent,Unit>({descriptor, event ->
logger.lifecycle("test: " + descriptor + ": " + event.message )
Unit
}))
afterTest(KotlinClosure2<TestDescriptor,TestResult,Unit>({descriptor, result ->
logger.lifecycle("test: $descriptor: $result")
Unit
}))
}
}
the<AppEngineStandardExtension>().apply {
deploy { // deploy configuration
projectId = System.getenv('GOOGLE_CLOUD_PROJECT')
version = "1"
}
}
Figuring this all out
Buildscript
buildscript { // Configuration for building
repositories {
jcenter() // Bintray's repository - a fast Maven Central mirror & more
mavenCentral()
}
dependencies {
classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.2.0' // If a newer version is available, use it
}
}
There are some general Groovy vs Kotlin syntax changes we will need to handle here and in the rest of the code:
- single quotes need to become double quotes
- methods like classpath, implementation, testImplementation need parenthesis around them.
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath("com.google.cloud.tools:appengine-gradle-plugin:2.4.1")
}
}
Plugins
apply plugin: 'java' // standard Java tasks
apply plugin: 'war' // standard Web Archive plugin
apply plugin: 'com.google.cloud.tools.appengine' // App Engine tasks
Originally I had wanted to use the new "plugins" syntax for adding the appengine plugin but it seems that it only works if the plugin is available in the Gradle Plugins Repository. So we have to use the older "apply" syntax instead.
plugins {
war
kotlin("jvm") version "1.4.21"
kotlin("plugin.spring") version "1.4.21"
kotlin("plugin.jpa") version "1.4.21"
}
apply(plugin = "com.google.cloud.tools.appengine")
Dependencies
dependencies {
compile 'com.google.appengine:appengine-api-1.0-sdk:+' // Latest App Engine Api's
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
compile 'jstl:jstl:1.2'
// Add your dependencies here.
// compile 'com.google.cloud:google-cloud:+' // Latest Cloud API's http://googlecloudplatform.github.io/google-cloud-java
testCompile 'junit:junit:4.12'
testCompile 'com.google.truth:truth:0.33'
testCompile 'org.mockito:mockito-all:1.10.19'
testCompile 'com.google.appengine:appengine-testing:+'
testCompile 'com.google.appengine:appengine-api-stubs:+'
testCompile 'com.google.appengine:appengine-tools-sdk:+'
}
In the Groovy DSL you can use deprecated "compile" syntax for adding dependencies as well as the newer "implementation" syntax. We can only use the later in the Kotlin DSL.
I also updated the plugin version as the comment in the original example suggests.
dependencies {
implementation("com.google.appengine:appengine-api-1.0-sdk:+") // Latest App Engine Api's
providedCompile("javax.servlet:javax.servlet-api:4.0.1")
implementation("jstl:jstl:1.2")
// Add your dependencies here.
// Testing
testImplementation("junit:junit:4.12")
testImplementation("com.google.truth:truth:1.1")
testImplementation("org.mockito:mockito-all:1.10.19")
testImplementation("com.google.appengine:appengine-testing:+")
testImplementation("com.google.appengine:appengine-api-stubs:+")
testImplementation("com.google.appengine:appengine-tools-sdk:+")
}
Deploy and stageStandard tasks depend on the test task
// Always run unit tests
appengineDeploy.dependsOn test
appengineStage.dependsOn test
This answer on Stack Overflow explains the syntax I used for getting the plugin's deploy task and stageStandard task to depend on the test task.
One tricky part is figuring out the correct type to supply to withType. The good news is if you get the type wrong gradle will complain with something like:
The task 'appengineDeploy' (com.google.cloud.tools.gradle.appengine.core.DeployTask) is not a subclass of the given type (org.gradle.api.tasks.compile.JavaCompile).
So you can figure it out through trial and error...
tasks.withType<com.google.cloud.tools.gradle.appengine.core.DeployTask> {
dependsOn("test")
}
tasks.withType<com.google.cloud.tools.gradle.appengine.standard.StageStandardTask> {
dependsOn("test")
}
Deploy block
appengine { // App Engine tasks configuration
deploy { // deploy configuration
projectId = System.getenv('GOOGLE_CLOUD_PROJECT')
version = '1'
}
}
This issue on Github explains the necessary syntax for the final deploy block.
the<AppEngineStandardExtension>().apply {
deploy { // deploy configuration
projectId = System.getenv('GOOGLE_CLOUD_PROJECT')
version = "1"
}
}
The tests block
test {
useJUnit()
testLogging.showStandardStreams = true
beforeTest { descriptor ->
logger.lifecycle("test: " + descriptor + " Running")
}
onOutput { descriptor, event ->
logger.lifecycle("test: " + descriptor + ": " + event.message )
}
afterTest { descriptor, result ->
logger.lifecycle("test: " + descriptor + ": " + result )
}
}
First change we need to do is wrap the block in a tasks block.
This conversion is also tricky because the Groovy DSL expects Groovy closures and we need to convert them to Kotlin closures. There is a small example on Github which covers how to use this "closureOf" method to handle the conversion. Note that when dealing with a closure that takes two arguments we need to use "KotlinClosure2" instead which I figured out from this blog.
An additional tricky part with this is we need to explicitly provide the type of the arguement(s) to closureOf and KotlinClosure2. In my case I was able to figure out the type by looking at the documentation for the test tasks which includes the full method signatures with types. This let me understand that "beforeTest" needs a TestDescriptor, "onOutput" needs TestDescriptor and TestOutputEvent, and "afterTest" needs TestDescriptor,TestResult.
tasks {
test {
useJUnitPlatform()
testLogging.showStandardStreams = true
beforeTest(closureOf<TestDescriptor> {
logger.lifecycle("test: $this Running")
})
onOutput(KotlinClosure2<TestDescriptor,TestOutputEvent,Unit>({descriptor, event ->
logger.lifecycle("test: " + descriptor + ": " + event.message )
Unit
}))
afterTest(KotlinClosure2<TestDescriptor,TestResult,Unit>({descriptor, result ->
logger.lifecycle("test: $descriptor: $result")
Unit
}))
}
}
And its as "simple" as that!