Kotlin + Gradle = Jenkins plugin
Запилил тут плагин для Jenkins на Kotlin. Делюсь впечатлениями.
Зачем?
Очень уж злит меня, как у нас на работе дела с документацией обстоят. Порой кажется, что лучше бы её вовсе не было, чем была такая, какая есть.
А всё потому что мы используем SharePoint. Раньше использовали Google Apps, было чуть проще, но сейчас поддержание документации в актуальном виде превращается в действительно нетривиальный трюк, похожий на жонглирование булавами. Типа: «разблокируй документ, открытый на редактирование уволившимся сотрудником». Ага, совместное редактирование в SharePoint похоже на SVN, где вы можете «залочить» файл. В общем, может O365 и крутая штука для манагерья, но, ИМХО, нормальные люди не должны с этим сталкиваться.
Мы, кстати, просто склонировали тот документ в новый.
Doktor
К счастью, у нас есть ещё и старенький Confluence, за который пока ещё уплочено.
А ещё, я люблю Markdown и Asciidoc(tor), а не эту пародию на UX, которую предлагают все эти коллаборативные тулзы. Мне нравится, как работают wiki-страницы и pages на GitHub.
Погуглил, не нашёл ничего достойного, решил написать свой велосипед, который будет заливать Markdown и Asciidoc в Confluence.
Всё оказалось довольно просто.
Gradle Kotlin DSL
Решил полностью перейти на Kotlin DSL в своих проектах.
Вердикт: уже юзабельно.
В новых версиях «причесали» API, нашли способы сделать его более похожим на привычный Groovy, но сохранить типобезопасность.
Например, аксессоры для конфигураций и экстеншенов.
Больше не нужно писать "compile"("g:a:1.0")
.
Работает медленно, но, зато, с хорошим автодополнением и проверкой валидности.
Иногда, правда, отваливается поддержка со стороны IDEA, фиксы прилетают в течение недели, а с тулбоксом и откатится не проблема. Ну и DSL в некоторых местах уродлив (спасибо динамической природе Groovy).
Было на Groovy:
developers {
developer {
id 'madhead'
name 'Siarhei Krukau'
email '[email protected]'
}
}
Стало на Kotlin:
developers = this.Developers().apply {
developer(delegateClosureOf<JpiDeveloper> {
setProperty("id", "madhead")
setProperty("name", "Siarhei Krukau")
setProperty("email", "[email protected]")
})
}
Gradle JPI Plugin
Для сборки Jenkins плагинов в Gradle есть специальный плагин. Он, конечно же, заточен под Java и ничего не знает про Kotlin, так что обрабатываем надфилем.
localizer
перед компиляцией Kotlin:tasks.withType(KotlinCompile::class.java).all {
dependsOn("localizer")
}
dependencies {
...
// SezPoz используется для процессинга @hudson.Extension и прочих аннотаций
kapt("net.java.sezpoz:sezpoz:${sezpozVersion}")
}
// Предотвращаем кэширование KAPT
tasks.withType(KaptTask::class.java).all {
outputs.upToDateWhen { false }
}
tasks.withType(KaptGenerateStubsTask::class.java).all {
outputs.upToDateWhen { false }
}
Это всё! После этой небольшой настройки Gradle стал собирать вполне валидный JPI-файл со всеми зависимостями и манифестами.
Можно даже запустить Jenkins с плагином для тестирования: ./gradlew clean jpi server
.
Jenkins API
Тут, к сожалению, не всё так радужно. Хорошей документации по Jenkins API я не нашёл, есть вот такая. Но ведь всегда можно изучить сорцы других плагинов! Подсматривал тут и тут. А ещё, некоторые плагины, например Credentials Plugin, который я использовал, у себя на вики приводят список собственных пользователей. Можно пробежаться по нему и найти нужные примеры кода. Ну и конечно же всегда можно спросить у топчиков в Gitter.
Что я понял о Jenkins API:
-
Всё, что отрисовывается на UI должно быть Describable, иметь Descriptor.
-
Повторяющиеся элементы конфигурации (как, например, инсталляторы JDK) должны быть
Describable
, поэтому нельзя просто так сделать список примитивов или строк, нужно писать обёртки. Ужасно! -
Всё, что будет использоваться в pipeline, должно быть
Serializable
. -
Асинхронные Pipeline-степы не нужны.
Rx Java 2
Я с самого начала использовал асинхронный API для работы с воркспейсом (на самом деле, не пожалел), и задумался о том, во что его обернуть. Корутин я пока побаиваюсь, решил попробовать Rx Java / Kotlin. Тем более, тут и вторая версия недавно подоспела.
Что ж. Я бы не сказал, что API идеален: десятки factory-методов, сотни перегруженных операторов у каждого типа. Типов много легко в них запутаться. А SAM-конверсии и вовсе сыграли со мной злую шутку!
.flatMap {
Observable.fromFuture(it).onExceptionResumeNext { Observable.empty<Int>() }
}
.flatMap {
Observable.fromFuture(it).onExceptionResumeNext ( Observable.empty<Int>() )
}
Оба вызова компилируются, имеют одинаковые типы (IDEA не заподозрит подвоха), только первый зависнет, а второй «проглотит» ошибки, как и ожидается.
Но Rx Java, это именно тот киберпанк API, который мы заслужили и придётся использовать именно его.
Долго пытался прикрутить Apache HTTP Components в поток данных, но потом вспомнил про замечательный Fuel.
Оказалось, у Fuel даже есть поддержка Rx типов из коробки.
Отличная либа, но не без изъяна: например, респонзы можно преобразовывать в типы с помощью интерфейса ResponseDeserializable
, но в реквесты можно передавать только строки и потоки (нету аналога типа, RequestSerializable
).
AsciidoctorJ / flexmark-java
Рендерить AsciiDoc на JVM можно только одним способом - AsciidoctorJ. Оригинальный Asciidoctor мне нравится. Я юзал его через Rake, подсмотрел этот способ у авторов Pro Git 2, когда мутили с ребятами перевод на русский. О, далёкий 2014-й!
AsciidoctorJ унаследовал от своего идейного вдохновителя простоту API, но из-за использования JRuby получил некоторые проблемы в окружениях с хитрым класслоадингом. Я подозреваю, что внутри Jenkins не OSGi, но рецепт подошёл.
С Markdown всё проще. Есть достойная библиотека flexmark-java, она поддерживает всякие расширения, типа front matter (как раз было очень нужно), GFM-таблиц и прочего. API ещё проще, чем у AsciidoctorJ. Работает с любым класслоадингом без лишних телодвижений.
Вердикт: обе либы хороши, но flexmark вообще няшка.
Публикация в Jenkins
Чтобы плагин появился в центре обновлений, его нужно опубликовать в репы Jenkins. При подключении JPI плагина паблишинг настраивается автоматически, нужны лишь валидные креды. Ещё, неплохо бы перенести репу на GH в сообщество Jenkins.
По порядку:
-
Заводим акк на https://accounts.jenkins.io. Это даёт доступ в JIRA, где далее будут заводиться тикеты. Этот же акк используется для аплоада в Artifactory (репа с плагинами).
-
Создаём таск в
HOSTING
проекте в JIRA. Нужно просто заполнить все обязательные параметры в форме, всё очень линейно. -
Ждём пару дней, пока плагин ревьювнут, отвечая в процессе на вопросы. К моему плагину возникли вопросы, так как, похоже, никто ещё не использовал Gradle Kotlin DSL для билда и суппорт уточнил, может ли этот плагин вообще быть собран без
build.gradle
. -
Как только таск выполнят, придёт приглашение в
jenkinsci
комьюнити на GH, а у плагина появится там форк. С этого момента уже можно начинать считать себя крутым. -
Форкаем этот проект и добавляем туда YAML (по примеру тысяч уже лежащих рядом) с правами на аплоад в определённые директории на Artifactory. Искомая директория - Maven координаты.
-
Отправляем PR и уже через пару часов его принимают и накатывают ACL. С этого момента можно паблишить плагин в Artifactory, а самооценка вырастает на 9К пунктов.
-
Хорошим тоном будет ещё сходить в Confluence (как иронично!) и создать там страничку про плагин. Разметку, опять же, лучше взять из соседнего файлика.
В целом, мне очень понравился весь процесс. Линейный, простой, гладкий. Эх, нам бы такие процессы на работу!
Итоги
За три недели по вечерам я написал вполне рабочую штуковину. Я потратил около ста часов и пятнадцати баксов. Деньги ушли на оплату c3.large EC2 инстанса, ибо Confluence оказался довольно прожорливым и тормозил мой бедный ноут, когда я запускал его локально в Docker’е. Я успел до истечения срока действия триальной лицензии, но, думаю, Atlassian продлит её, если я хорошо попрошу. Меня друг обнадёжил, потративший год своей жизни на работу на Atlassian.
А ещё, у меня на GH появился беджик jenkinsci (по чесноку, ради этого и старался).
Кстати, плагин можно собрать через ./gradlew clean jpi
и попробовать / потестировать у себя, если, конечно, есть желание или необходимость.