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")
}
Настройка Kotlin Annotation Processing:
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:

  1. Всё, что отрисовывается на UI должно быть Describable, иметь Descriptor.

  2. Повторяющиеся элементы конфигурации (как, например, инсталляторы JDK) должны быть Describable, поэтому нельзя просто так сделать список примитивов или строк, нужно писать обёртки. Ужасно!

  3. Всё, что будет использоваться в pipeline, должно быть Serializable.

  4. Асинхронные Pipeline-степы не нужны.

  5. Как и асинхронные операции над воркспейсом.

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.

По порядку:

  1. Заводим акк на https://accounts.jenkins.io. Это даёт доступ в JIRA, где далее будут заводиться тикеты. Этот же акк используется для аплоада в Artifactory (репа с плагинами).

  2. Создаём таск в HOSTING проекте в JIRA. Нужно просто заполнить все обязательные параметры в форме, всё очень линейно.

  3. Ждём пару дней, пока плагин ревьювнут, отвечая в процессе на вопросы. К моему плагину возникли вопросы, так как, похоже, никто ещё не использовал Gradle Kotlin DSL для билда и суппорт уточнил, может ли этот плагин вообще быть собран без build.gradle.

  4. Как только таск выполнят, придёт приглашение в jenkinsci комьюнити на GH, а у плагина появится там форк. С этого момента уже можно начинать считать себя крутым.

  5. Форкаем этот проект и добавляем туда YAML (по примеру тысяч уже лежащих рядом) с правами на аплоад в определённые директории на Artifactory. Искомая директория - Maven координаты.

  6. Отправляем PR и уже через пару часов его принимают и накатывают ACL. С этого момента можно паблишить плагин в Artifactory, а самооценка вырастает на 9К пунктов.

  7. Хорошим тоном будет ещё сходить в Confluence (как иронично!) и создать там страничку про плагин. Разметку, опять же, лучше взять из соседнего файлика.

В целом, мне очень понравился весь процесс. Линейный, простой, гладкий. Эх, нам бы такие процессы на работу!

Итоги

За три недели по вечерам я написал вполне рабочую штуковину. Я потратил около ста часов и пятнадцати баксов. Деньги ушли на оплату c3.large EC2 инстанса, ибо Confluence оказался довольно прожорливым и тормозил мой бедный ноут, когда я запускал его локально в Docker’е. Я успел до истечения срока действия триальной лицензии, но, думаю, Atlassian продлит её, если я хорошо попрошу. Меня друг обнадёжил, потративший год своей жизни на работу на Atlassian.

А ещё, у меня на GH появился беджик jenkinsci (по чесноку, ради этого и старался).

Кстати, плагин можно собрать через ./gradlew clean jpi и попробовать / потестировать у себя, если, конечно, есть желание или необходимость.