I spent money on a domain so I might as well use it.

Google Cloud Platform with Gradle Kotlin DSL

Converting the Groovy Gradle configuration for Google Cloud Platform to the 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!