Jenkins Active Choice
Есть плагин который позволяет динамически формировать значения параметров, например динамически подтягивать список веток в git (НО нельзя динамически создать параметры, и у меня не вышло сделать скрытыми параметры в зависимости от того что выбрано в другом параметре)
В чем сложность ?
В целом решается задача достаточно несложно - особенно когда есть одно приложение и один репозиторий, но ситуация другая когда приложений и репозиториев (а соответственно и ключей к ним) становится много
Это тоже не кажется сложным, просто использовать для каждого приложения свой скрипт где будет "зашит" свой адрес и ключ.
НО в этом случае получаем множество скриптов которые нужно будет синхронно обновлять и правильно вызывать
Хочется использовать ОДИН скрипт который единственный модифицировать при добавлении приложения.
Постановка задачи
- Деплоймент приложения состоящего из множества сервисов: app1, app2, app3 ... appN. Число сервисов может увеличиваться.
- Сервисы могут деплоится в любых сочетаниях - один, два, все сразу, первый и третий, только номер N ...
- Для сервиса который выбран для деплоймента предоставлять выбор бранчи, для сервиса который не выбран - не предоставлять
Пример интерфейса
- Слева - выбран один сервис и бранча для него, второй сервис не выбран и соответвенно выбор бранчи для него недоступен.
- Справа - выбрано два сервиса и бранчи для обоих сервисов
Как решается задача
Собственно задача сводится к тому что бы определить изменение какого параметра вызвало запуск скрипта
Так как значение того параметра что вызвало запуск скрипта передается в скрипт (имя переменной совпадает с именем параметра и в примере это будет app1 или app2 или ... appN)
то этим можно воспользоваться - создать список сервисов с информацией о них и их именами
List reposAndKeys = [
[
"name": "app1",
"repo": "git@github.com:app2.git",
"repoKeyId": "key1",
],
[
"name": "app2",
"repo": "git@github.com:app2.git",
"repoKeyId": "key2",
],
...
Составить список какие имена могут быть доступны?
List applications = []
reposAndKeys.each { repoAndKey ->
applications.add(repoAndKey["name"])
}
Проверить существование переменной (и ее значение) можно вот таким способом: если скрипт вызван изменением значения параметра app1 то переменная app1 будет доступна внутри кода а переменная app2 нет (это верно для любого appN)
applications.each { it ->
try {
// если it указывает на несуществующее значение то тут будет исключение
println("${it} = ${this[ it ]}")
// Если чекбокс не выбран - то в значении переменной (имя переменной это имя параметра) будет пустая строка
// тут нужно добаить другие проверки если планируется использовать другиме типы параметов
if ("${this[ it ]}" == "") {
isDisabled = true
}
triggeredByParameterNames.add("${it}")
} catch(Exception Ex) {
println("${it}")
println(Ex)
}
}
Некоторые дополнительные вещи вроде обработки ситуаций когда не выбран не один appN но скрипт запущен или ошибочно сконфигурировано более чем 1 appN как параметр вызывающей пересчет сиска бранчей
Пример кода
Описаниа в Jenkins Job Builder
- job:
name: 'common/frontend/build/build-all-frontend-applications'
project-type: pipeline
dsl:
!include-raw: 'pipelines/common/frontend/build/build-all-frontend-applications.groovy'
concurrent: true
build-discarder:
artifactDaysToKeep: '30'
artifactNumToKeep: '-1'
daysToKeep: '30'
numToKeep: '30'
properties:
- copyartifact:
projects: "*"
parameters:
- active-choices:
name: "app1"
description: "Deploy app1"
script:
groovy: "return ['app1:selected']"
use-groovy-sandbox: false
choice-type: "checkboxes"
- active-choices-reactive:
name: "app1_git_branch"
description: "Git Branch For App1"
script:
groovy:
!include-raw: 'pipelines/common/frontend/build/active-choice-scripts/branch-selector.groovy'
use-groovy-sandbox: false
choice-type: single-select
enable-filters: true
filter-starts-at: 1
referenced-parameters: "app1"
- active-choices:
name: "app2"
description: "Deploy app2"
script:
groovy: "return ['app2:selected']"
use-groovy-sandbox: false
choice-type: "checkboxes"
- active-choices-reactive:
name: "app2_git_branch"
description: "Git Branch For App2"
script:
groovy:
!include-raw: 'pipelines/common/frontend/build/active-choice-scripts/branch-selector.groovy'
use-groovy-sandbox: false
choice-type: single-select
enable-filters: true
filter-starts-at: 1
referenced-parameters: "app2"
Код branch selector
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import jenkins.model.Jenkins
// ВНИМАНИЕ - println пишет В ЛОГ и читать сообщение надо в логе дженкинса
// НА современных линуксах например:
// journalctl -u jenkins --since -1m -f
// Список приложений и их доступов
List reposAndKeys = [
[
"name": "app1",
"repo": "git@github.com:app1.git",
"repoKeyId": "app1-git-deployment-key",
],
[
"name": "app2",
"repo": "git@github.com:app2.git",
"repoKeyId": "appN-git-deployment-key",
],
...
[
"name": "appN",
"repo": "git@github.com:appN.git",
"repoKeyId": "appN-git-deployment-key",
]
]
// Получение списка всех бранчей
def getAllBranches( String url, String credentialID, Boolean activeChoice = false,
String defaultBranch = 'master', Boolean includeTags = false, String logTag = "") {
def jenkinsCredentials = CredentialsProvider.lookupCredentials(
com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey,
Jenkins.instance
);
def key = jenkinsCredentials.findResult { it.id == credentialID ? it.privateKey : null }
if (!key) {
return "[${logTag}]Error: credentials not found"
} else {
print("[${logTag}] Key Found: \n ${key} \n")
}
// иногда с первого раза не удается получить все бранчи
Integer attemptsLeft = 5
def out = new StringBuilder()
def err = new StringBuilder()
while(attemptsLeft > 0) {
try {
print("[${logTag}] Starting process\n")
Process process = ['ssh-agent', 'bash', '-c', "echo '" + key + "' | ssh-add - 2> /dev/null && git ls-remote -t -h " + url].execute()
process.consumeProcessOutput(out, err)
print("[${logTag}] Waiting for started process\n")
process.waitFor()
print("[${logTag}] Process is finished\n")
print("[${logTag}] Out: \n${out}\n\n")
if (out == null) {
attemptsLeft = attemptsLeft - 1
print("[${logTag}] Output ${out} is null. attemptsLeft = ${attemptsLeft} \n")
continue
}
if (out.size() == 0) {
attemptsLeft = attemptsLeft - 1
print("[${logTag}] Output ${out} is empty. attemptsLeft = ${attemptsLeft} \n")
continue
}
print("[${logTag}] Out: ${out} looks OK\n")
break
} catch(Exception Ex) {
println("[${logTag}] " + Ex.toString())
throw new Exception(Ex)
}
} // while
if (err.size() > 0) {
return err
}
if (out.size() > 0) {
def branches = out.readLines().collect {
it.split()[1]
}.findAll {
it.startsWith('refs/heads/') || includeTags
}.collect {
it.replaceAll('refs/heads/', '').replaceAll('refs/tags/', '')
}
if (activeChoice) {
def defaultBranchIndex = branches.indexOf(defaultBranch)
if (defaultBranchIndex >= 0) {
branches.set(defaultBranchIndex, defaultBranch + ':selected')
}
}
println("Branches found: ${branches}")
return branches
}
}
List applications = []
List branches = []
// Просто список приложений - только имена
reposAndKeys.each { repoAndKey ->
applications.add(repoAndKey["name"])
}
println("applications = ${applications}")
//Флаг который нужен что бы определить когда
// чекбокс снят
Boolean isDisabled = false
List triggeredByParameterNames = []
// Параметр который триггерит вызов этого скрипта должен называться так же как одно
// из имен приложений
// Логика тут примерно такая
// - есть несколько параметров приложений, например app1, app2, app3 ... appN
// - есть массив конфигураций с конфигурацией для каждого приложение - ИМЯ, адрес, ключ ..
// - ИМЯ приложения в этом массиве ДОЛЖНО совпадать с именем переменной которая является Referenced parameters
// Другими словами имя параментра (например app1) должно совпадать с именем ("name": "app1") в массиве конйигураций
// При старте скрипта в нем создается переменная которая указана в Referenced parameters с таким же именем
// т.е. всегда будет доступна одна из переменных - app1 ИЛИ app2 ИЛИ ... ИЛИ appN но проблема
// в том что в коде заранее не известно имя этой переменной (так как код скрипта полностью одинаков и делать
// несколько копий скрипта, каждая для своего приложения, не хочется)
//
// Код ниже пытается определить какая из переменных определена, для этого он последовательно пробует прочитать значения
// всех возможных и в случае успеха - запоминает какая переменная доступна
// Есди переменная доступна но ее значение - пуста строка то это значит что чекбокс снят и нужно выдать пустой список
// Если переменная доступна и ее значение не равно пустой строке - то это значит что чекбокс выбран и нужно получить список
// бранчей
// (Пустрая строка или что-то другое - зависит от ТИПА переменной и этот код не универсален)
applications.each { it ->
try {
// если it указывает на несуществующее значенеи то тут будет исключение
println("${it} = ${this[ it ]}")
// Если чекбокс не выбран - то в значении переменной (имя переменной это имя параметра) будет пустая строка
// тут нужно добаить другие проверки если планируется использовать другиме типы параметов
if ("${this[ it ]}" == "") {
isDisabled = true
}
triggeredByParameterNames.add("${it}")
} catch(Exception Ex) {
println("${it}")
println(Ex)
}
}
print("triggeredByParameterNames = ${triggeredByParameterNames}\n")
print("triggeredByParameterNames size = ${triggeredByParameterNames.size()}\n")
if (isDisabled) {
print("Application is not selected for deploy\n")
return []
}
// В какой-то момент времени происходит выхов и НИ одна переменная не оказывается доступна - что бы не ловить исключения
// аозвращаю пустой List
if (triggeredByParameterNames.size == 0 ) {
print("No valid applivation found (Possible reason: script misconfiguration or started without required parameters)\n")
return []
}
// Проверить что вызвано только одним приложением
// Это означает что для каждого приложения должен быть свой параметр имени ветки
// и нельзя сделать так что бы пересчет этого параметра триггерился 2 или более
// параметрами (так как это в целом не имеет смысла)
if (triggeredByParameterNames.size > 1 ) {
return ["Triggered by multiple parameters is not supported, please check Referenced parameters setting, only one parameter is allowed "]
}
String logTag = triggeredByParameterNames[0].toString()
repoAndKey = reposAndKeys.findAll { it.name == triggeredByParameterNames[0] }
print("[${logTag}] Parameters are: ${repoAndKey}\n")
String credentialsId = repoAndKey[0]['repoKeyId']
String url = repoAndKey[0]['repo']
// Собственно вызов получения списка бранчей
getAllBranches(url, credentialsId, true, "master", false, logTag)