TeamCity
Powerful CI/CD for DevOps-centric teams
Kotlin Configuration Scripts: Extending the TeamCity DSL
This is part four of the five-part series on working with Kotlin to create Configuration Scripts for TeamCity.
- An Introduction to Configuration Scripts
- Working with Configuration Scripts
- Creating Configuration Scripts dynamically
- Extending the TeamCity DSL
- Testing Configuration Scripts
TeamCity allows us to create build configurations that are dependent on each other, with the dependency being either snapshots or artifacts. The configuration for defining dependencies is done at the build configuration level. For instance, assuming that we have a build type Publish that has a snapshot dependency on Prepare Artifacts, we would define this in the build type Publish in the following way
class Publish : BuildType({ uuid = "53f3d94a-20c6-43a2-b056-baa19e55fd40" extId = "Test" name = "Build" vcs { . . . } steps { . . . } dependencies { dependency(Prepare_Artifacts) { snapshot { onDependencyFailure = FailureAction.FAIL_TO_START } } } })
and in turn if Prepare Artifacts had dependencies on previous build configurations, we’d define these in the dependencies segment of its build configuration.
TeamCity then allows us to visually see this using the Build Chains tab in the user interface
Defining the pipeline in code
Pipeline is a sequence of phases, phase is a set of buildTypes, each buildType in a phase depends on all buildTypes from the previous phase. This can handle simple but common kind of build chains where some build produces an artifact, several builds test it in parallel and the final build deploys the result if all its dependencies are successful.
It would often be beneficial to be able to define this build chain (or build pipeline) in code, so that we could describe what’s going on from a single location. In essence, it would be nice to be able to define the above using the Kotlin DSL as so
object Project : Project ({ . . . pipeline { phase("Compile all clients") { + HttpClient + FluentHc + HttpClientCache + HttpMime } phase("Prepare artifacts") { + PrepareArtifacts } phase("Deploy and publish to CDN") { + Publish } } })
Defining this at the `Project.kt` level in our DSL, would give us a good oversight of what’s taking place.
The issue is though that currently the TeamCity DSL does not provide this functionality. But that’s where Kotlin’s extensibility proves quite valuable as we’ll see.
Creating our own Pipeline definition
Kotlin allows us to create extension functions and properties, which are a means to extend a specific type with new functionality, without having to inherit from these. When passing extension functions as arguments to other functions (i.e. higher-order functions), we get what we call in Kotlin Lambdas with Receivers, something we saw in this series already when Generalising feature wrappers in the second part of this series. We apply the same concept here to create our `pipeline` DSL
class Pipeline { val phases = arrayListOf<Phase>() fun phase(description: String = "", init: Phase.() -> Unit = {}) { val newPhase = Phase() newPhase.init() phases.lastOrNull()?.let { prevPhase -> newPhase.buildTypes.forEach { it.dependencies { for (dependency in prevPhase.buildTypes) { snapshot(dependency) } } } } phases.add(newPhase) } } class Phase { val buildTypes = hashSetOf<BuildType>() operator fun BuildType.unaryPlus() { buildTypes.add(this) } } fun Project.pipeline(init: Pipeline.() -> Unit = {}) { val pipeline = Pipeline() pipeline.init() //register all builds in pipeline pipeline.phases.forEach { phase -> phase.buildTypes.forEach { bt -> this.buildType(bt) } } }
What the code above is doing is define a series of new constructs, namely `pipeline` and `phase`.
The code then takes the contents of what’s passed into each of these and defines, under the covers, the dependencies. In essence, it’s doing the same thing we would do in each of the different build configurations, but from a global perspective defined at the `Project` level.
In order to pass in the configuration to the pipeline as we saw earlier, we merely reference the specific build type (`HttpClient`, `Publish`, etc.), assigning it to a variable
`val HttpClient = BuildType(….)`
Flexibility of defining our own pipeline constructs
It’s important to understand that this is just one of many ways in which we can define pipelines. We’ve used the terms `pipeline` and `phase`. We could just as well have used the term `stage` to refer to each phase, or `buildchain` to refer to the pipeline itself (and thus align it with the UI). In addition to how we’ve named the constructs, and more importantly, is how the definition is actually constructed. In our case, we were interested in having this present in the `Project` itself. But we could just as well define a different syntax that is used at the build type level. The ability to easily extend the TeamCity DSL with our own constructs, provides us with this flexibility.