in kotlin dsl devops ~ read.
Kotlin, DSL  и все все все

Kotlin, DSL и все все все

В одном из факультативных проектов мне понадобилось заавтоматизировать некоторые не слишком сложные, но рутинные действия. Пробой пера для решения проблемы выступил кем-то любимый, а кем-то не очень, Ansible по всё тем же понятным причинам: просто, быстро, и даже работает. И не сказать, чтобы он не решил проблемы, наоборот, очень даже, но это тот случай когда лекарство получилось горьким, и пить его совершенно не хочется, надеясь что быть может боль пройдёт и так. Про этот опыт я расскажу чуть позже, сегодня не об этом.

Но если уж очень кратко, то в какой-то момент я осознал, что шлёпать портянки в виде плэйбуков занятие неинтересное и муторное, а необходимость копипаста просто убивала саму идею на корню. Чтобы пофиксить проблему, как минимум, нужно было написать модуль для Ansible-а, а я не очень хорошо знаю Python, и почему-то не горю желанием его пока что изучать. Зато мне известны другие, не менее хорошие языки программирования, как то Java или Groovy. И я начал смотреть в их сторону, чтобы сделать тоже самое, описав свои действия в виде некоторого DSL-а, который можно было бы запустить хоть из любимой IDE, хоть из командной строки, да и даже просто с помощью одноразового презерватива docker-контейнера.

Чистую Java я не стал рассматривать хотя бы потому, что для скриптинга она мне кажется слегка вербозной. Поэтому из кандидатов оставались более-менее знакомый Groovy, и совсем менее знакомый Kotlin. И тот, и другой язык дают некоторый синтаксический сахар для того, чтобы писать скрипты и DSL в компактном и понятном виде без крови из глаз и точек с запятыми в каждой строке.

Насмотревшись на поддержку инструментария для Gradle в IDEA я решил попробовать их собственную разработку, поскольку уж code completion обязан работать не в пример лучше, плюс нативная поддержка от компании производителя и прочие плюшки должны облегчить жизнь как мне самому, так и коллегам в не слишком отдалённой перспективе.

Ну и всегда интересно изучить что-то новое, правда?)

В начале было хорошо

Итак, сначала про хорошие и позитивные моменты.

  • Писать на Котлине действительно приятно
  • Много разнообразного "сахара", который помогает писать меньше кода. Как в DSL, так и в его реализации
  • Хорошая поддержка в IDE и в Gradle. Проблем с написанием, запуском и даже сборкой docker-контейнера не возникло от слова совсем

Достаточно быстро я накидал первую реализацию с возможностью писать DSL в лучших традициях Groovy-скриптов:

steps {  
   empty("init") {
      doSmth {
      }
   }
}

Приятно и то, что всё это проверяется во время компиляции, так что совсем уж кривой и нерабочий DSL написать не получится.

Про тестирование

Одним из первых моментов, которые хотелось бы решить, был вопрос тестирования, поскольку изменения хочется вносить часто, и при этом оставлять уже написанные скрипты рабочими.

Из фреймворков для тестирования я решил остановиться на Spek, потому что он чем-то похож на небезизвестный Spock и даёт возможность описать тесты в приятном и понятном виде, а это дорогого стоит.

В примере ниже на самом деле будут два разных теста, при этом логически тест разделён на три вложенных блока: describe, on и it.

object StepsCollapserSpek : Spek ({  
    describe("Collapser") {
        val steps = listOf(
                Step("name", "value1"),
                Step("name", "value2"),
                Step("name", "value3")
        )

        on("collapse steps") {
            val collapsed = stepsCollapser.collapse(steps)

            it("should be only one") {
                assertEquals(1, collapsed.size)
            }

            it("should has propertyValue as value3") {
                assertEquals("value3", collapsed[0].value)
            }
        }
    }
})

В первом блоке мы указываем что именно мы определяем, в блоке on делаем некоторое логическое действие, а в блоке it проверяем результат исполнения.

Нужно понимать, что тут каждый it это фактически отдельный тест, в котором мы можем сделать один assert, что куда лучше одного теста с множественными assert-ами.

А теперь пару ложек дёгтя. Во-первых, чтобы запускать Spek-тесты из IDE, вам нужно ставить дополнительный плагин. Это не то, чтобы критично, но всё же дополнительное телодвижение.

Во-вторых, в gradle build-скрипте нужно сотворить немного магии, и при любой ошибке либо тесты молча не будут запускаться, либо будут валиться очень странные ошибки:

buildscript {  
    //versions are needed to be fit
    //with each other
    ext.kotlin_version = '1.1.4-2'
    ext.spek_version = '1.1.4'
    ext.junit_runner_version = '1.0.0-RC3'

    repositories {
        //repos
    }

    dependencies {
        classpath "org.junit.platform:junit-platform-gradle-plugin:$junit_runner_version"
    }
}

//another plugins here

apply plugin: 'org.junit.platform.gradle.plugin'

//include spek tests to junit platform
junitPlatform {  
    platformVersion '1.0.0-RC3'
    filters {
        engines {
            include 'spek'
        }
    }
}

//usage of custom jetbrains repo
repositories {  
    maven { url "https://dl.bintray.com/jetbrains/spek" }
    //other repos
}

dependencies {  
    //other deps first

    //spek tests
    testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
    testCompile "org.jetbrains.spek:spek-api:$spek_version"
    testCompile "org.junit.platform:junit-platform-runner:$junit_runner_version"
    testRuntime "org.jetbrains.spek:spek-junit-platform-engine:$spek_version"
}

В конечном счёте, помучившись некоторое время, можно заставить Spek работать, и он будет работать хорошо. По крайней мере с самими тестами, результатами их отображения и запуском (после правильной настройки, разумеется) у меня проблем не было, что оставило позитивные впечатления.

Про kotlin script

Очевидно, что писать DSL-скрипты нужно в виде скриптов. Как минимум это именно то, что мы хотим, правда? Ну не писать же для этого отдельную обёртку, подгружать скомпилированные классы и прочее. Так я думал до.

Итак. Kotlin script это фактически тот же самый Kotlin, только с имплицитными аргументами и возможностью запуска из командной строки файлов с расширением .kts.

Скажем, используя готовые конструкции можно написать очень просто скрипт, который будет исполнять некоторые наши шаги, а аргументы командной строки придут в имплицитной переменной args. Никакого main и прочей фигни.

import runner.*

runner.run {  
    steps {
        step1("firstStep") {
            applyArg(args[0])
        }
    }
}

Хорошая новость в том, что написание скриптов ничем особо не отличается от работы с теми же Kotlin-классами. Типизация, проверки при компиляции и прочие клёвые штуки работают как часы.

Ложка дёгтя однако заключается в том, что в родной же IDE запустить нужный вам скрипт не получится. Не получится по той простой причине, что classpath проекта, в котором пишется скрипт не подтягивается. Это сводит на нет почти все преимущества скриптинга, поскольку ну не одной же stdlib пользоваться. Баге на это дело уже почти полтора года. Печально.

Однако скрипты всё равно полезны хотя бы по тому, что их можно динамически подгружать в обычном коде, чем мы и воспользуемся:

val scriptName = parsedArgs.scriptName

val factory = ScriptEngineManager().getEngineByExtension("kts")!!

factory.eval(getResource("$scriptName.kts").readText())  

В итоге у нас будет консольная обёртка, которая одним из аргументов может принимать имя скрипта для запуска. Не слишком красивый workaround, но это лучше чем ничего.

Отдельно нужно отметить, что чтобы engine kts вообще появился нужно указать его в ресурсах: META-INF/services/javax.script.ScriptEngineFactory. Внутри файла необходимо прописать магическую строчку:

org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory  

Ну и не забыть подключить к сборке библиотеки:

compile "org.jetbrains.kotlin:kotlin-script-runtime:$kotlin_version"  
compile "org.jetbrains.kotlin:kotlin-compiler:$kotlin_version"  
compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version")  

Без этого NPE, господа!

Особенно доставило то, что без kotlin-script-util ничего не работает. Блин, но ведь это же util, Карл.

И про Docker

Самая скучная часть однако, потому что просто всё работает без лишних телодвижений. Используем плагин: com.bmuschko:gradle-docker-plugin:3.1.0 с элементарной настройкой:

mainClassName = 'my.package.ConsoleRunnerKt'  

И запускаем:

./gradlew dockerBuildImage

docker run --rm -it runner:1.0 -e dev -s customScript1  

Вместо заключения

Kotlin, несмотря на некоторые проблемы встреченные мной на пути, можно использовать в качестве инструмента для разработки и запуска DSL-скриптов. Возможность написать любой кастомный, удобный, компилируемый и подходящий под предметную область DSL на jvm-языке это круто.

Был бы Groovy в этой ситуации лучше? Возможно, но этого мы никогда не узнаем.

comments powered by Disqus