How to create TeamCity project with Kotlin DSL?

Berkay Yılmaz
5 min readJan 2, 2020

--

What is TeamCity?

JetBrains TeamCity is a user-friendly continuous integration (CI) server for developers and build engineers and easy to set up!

What can you do with TeamCity?

  • Run parallel builds simultaneously on different platforms and environments,
  • Optimize the code integration cycle and be sure you never get broken code in the repository,
  • Review on-the-fly test results reporting with intelligent tests re-ordering,
  • Run code coverage and duplicates finder for Java and .NET,
  • Customize statistics on build duration, success rate, code quality, and custom metrics

In the basically you can do above mentioned things in TeamCity. For more information please check the link.

So, Let’s get started!

What are domain-specific languages?

First of all, what are DSLs exactly and why should we use them? Let’s have a look at Wikipedia’s definition on DSLs:

A domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains.

Basically, a DSL is a language that focuses on just one particular part of an application. A general-purpose language, such as Kotlin or Java, on the other hand, can be used in multiple parts of one application.

Sample Kotlin DSL Project

I am going to show you how to create TeamCity projects in Kotlin DSL.

It is best to add a Kotlin DSL configuration to an existing project in TeamCity. First, create a new project in TeamCity by pointing to a repository URL. Just export the DSL code to your local computer. As a result, the new project will include one VCS root and a build configuration.

Now you need to work on exported project which includes some format. We want to extend it and add more features on it.

To start using Kotlin build scripts in TeamCity the Versioned Settings have to be enabled. Regardless of whether you are starting from scratch or you have an existing project. In the project configuration, under Versioned Settings, select the Synchronization enabled option, select a VCS root for the settings, and choose Kotlin format.

Exported DSL file can be added in Gitlab repository and use that as VCS Root in Versioned Setting.

In the Intellij, you can create below structure. In order to detect by TeamCity there is required file which is “pom.xml”.

settings.kts

This the root project file. createEnvironment function is called inside of settings.kts which create environment as Staging and Production. In the settings.kts there are created parameters and use Vault connection.

import jetbrains.buildServer.configs.kotlin.v2018_2.*import projects.network.Network

import jetbrains.buildServer.configs.kotlin.v2018_2.ParameterDisplay
import jetbrains.buildServer.configs.kotlin.v2018_2.Project
import jetbrains.buildServer.configs.kotlin.v2018_2.toId
import jetbrains.buildServer.configs.kotlin.v2018_2.vcs.GitVcsRoot
version = "2019.1"
project(Project)
object Project : Project({
/**
* Setup the project
*/

params {
param("aws_region", "us-east-1")
}

/**
* Setup the environments
*/
subProject(Staging)
subProject(Production)
})
val Staging = createEnvironment(
"Staging",
"staging",
"staging",
"staging",
"_AWS_ACCOUNT_ID_"
)
val Production = createEnvironment(
"Production",
"production",
"production",
"production",
"_AWS_ACCOUNT_ID_"
)
fun createEnvironment(envTitle: String, envKey: String, namespace: String, branchName: String, envAccountId: String): Project {
val infraVcsRoot = createInfraVCS(branchName)

return Project {
id(envTitle.toId())
name = envTitle

vcsRoot(infraVcsRoot)

params {
param("environment", envKey)
param("ami_path", "ami")
param("application_path", "application")
param("autoscaling_path", "autoscaling")
param("terraform_path", "general")
param("aws_db_secret_id", "staging/mssql/xxx")
param("aws_webserver_secret_id", "staging/webserver/windows")

text("env.AWS_SECRET_ACCESS_KEY", "%vault:/aws/creds/role-198701364222-teamcity!/secret_key%", display = ParameterDisplay.HIDDEN, readOnly = true, allowEmpty = true)
text("env.AWS_SESSION_TOKEN", "%vault:/aws/creds/role-198701364222-teamcity!/security_token%", display = ParameterDisplay.HIDDEN, readOnly = true, allowEmpty = true)
text("env.AWS_ACCESS_KEY_ID", "%vault:/aws/creds/role-198701364222-teamcity!/access_key%", display = ParameterDisplay.HIDDEN, readOnly = true, allowEmpty = true)

text("xxx_cert_chain", "%vault:/secret/xxx/certificate!/chain%", display = ParameterDisplay.HIDDEN, readOnly = true, allowEmpty = true)
text("xxx_cert_crt", "%vault:/secret/xxx/certificate!/crt%", display = ParameterDisplay.HIDDEN, readOnly = true, allowEmpty = true)
text("xxx_cert_key", "%vault:/secret/xxx/certificate!/key%", display = ParameterDisplay.HIDDEN, readOnly = true, allowEmpty = true)
}

subProject(Network(namespace, infraVcsRoot))
}
}

Above codes are going to create below sub projects

Let’s dive in “Staging” project!

As you can see there are some projects have been created as sub projects. On the left sidebar there are “Parameters” which has already created parameters with “param” command in above.

These parameters come from above configuration.

In my example I am going to cover only Network project part which creates example AWS resources with Terraform Infrastructure code.

Network.kt

In this file I created build steps which are I am going to show below and order them.

It’s really important to add id’s for files in order to prevent some conflicts!

package _Self.projects.network

import _Self.projects.network.buildTypes.*
import jetbrains.buildServer.configs.kotlin.v2018_2.Project
import jetbrains.buildServer.configs.kotlin.v2018_2.vcs.GitVcsRoot

class Network(envKey: String, vcsRoot: GitVcsRoot) : Project({
id("${envKey}_Network")
name = "Network"

/**
* Builds
*

buildType(TerraformApply(envKey, vcsRoot))
})

TerraformApply.kt

This file creates build steps in TeamCity which you can see console view below. You can write your scripts, AWS CLI codes, Terraform command line codes, etc…

package _Self.projects.network.buildTypes

import jetbrains.buildServer.configs.kotlin.v2018_2.BuildStep
import jetbrains.buildServer.configs.kotlin.v2018_2.BuildType
import jetbrains.buildServer.configs.kotlin.v2018_2.buildSteps.script
import jetbrains.buildServer.configs.kotlin.v2018_2.failureConditions.BuildFailureOnText
import jetbrains.buildServer.configs.kotlin.v2018_2.failureConditions.failOnText
import jetbrains.buildServer.configs.kotlin.v2018_2.vcs.GitVcsRoot

class TerraformApply(envKey: String, vcsRoot: GitVcsRoot) : BuildType({
id("${envKey}_Network_Terraform_apply")
name = "Network Terraform Apply"

vcs {
root(
vcsRoot
)
}

steps {
script {
name = "Pre-clean result files"
scriptContent = """
#!/bin/bash
rm -f result-apply.txt
""".trimIndent()
workingDir = "%terraform_path%"
}
script {
name = "Download Certificates"
scriptContent = """
mkdir -p ./cert
cd ./cert
cat <<EOT > chain
%xxx_cert_chain%
EOT

cat <<EOT > crt
%xxx_cert_crt%
EOT

cat <<EOT > key
%xxx_cert_key%
EOT
""".trimIndent()
}
script {
name = "Terraform Init & Apply"
workingDir = "%terraform_path%"
scriptContent = """
terraform init -backend-config="backend-configs/${envKey.toLowerCase()}.tf"
terraform apply -var "environment=${envKey.toLowerCase()}" -auto-approve | tee -a result-apply.txt || exit 1
""".trimIndent()
}
script {
name = "cleanup"
scriptContent = "rm -rf cert"
executionMode = BuildStep.ExecutionMode.ALWAYS
}
}

failureConditions {
failOnText {
conditionType = BuildFailureOnText.ConditionType.REGEXP
pattern = "Error:"
failureMessage = "Terraform reported an error"
stopBuildOnFailure = true
}
}

})

So we’ve done!

As you can see our final TeamCity configurations seem as below. There is button on right above which is called “Run” in order to run our build.

After you run build, you can check the build logs!

Enjoy!

References

--

--