JenkinsActiveChoice: различия между версиями

Материал из noname.com.ua
Перейти к навигацииПерейти к поиску
 
(не показана 21 промежуточная версия этого же участника)
Строка 3: Строка 3:
 
=Jenkins Active Choice=
 
=Jenkins Active Choice=
 
Есть плагин который позволяет динамически формировать значения параметров, например динамически подтягивать список веток в git (НО нельзя динамически создать параметры, и у меня не вышло сделать скрытыми параметры в зависимости от того что выбрано в другом параметре)
 
Есть плагин который позволяет динамически формировать значения параметров, например динамически подтягивать список веток в git (НО нельзя динамически создать параметры, и у меня не вышло сделать скрытыми параметры в зависимости от того что выбрано в другом параметре)
  +
  +
= В чем сложность ?=
  +
В целом решается задача достаточно несложно - особенно когда есть одно приложение и один репозиторий, но ситуация другая когда приложений и репозиториев (а соответственно и ключей к ним) становится много
  +
<BR>
  +
Это тоже не кажется сложным, просто использовать для каждого приложения свой скрипт где будет "зашит" свой адрес и ключ.
  +
<BR>
  +
НО в этом случае получаем множество скриптов которые нужно будет синхронно обновлять и правильно вызывать
  +
<BR>
  +
Хочется использовать ОДИН скрипт который '''единственный''' модифицировать при добавлении приложения.
  +
  +
  +
<BR>
  +
== Постановка задачи ==
  +
* Деплоймент приложения состоящего из множества сервисов: app1, app2, app3 ... appN. Число сервисов может увеличиваться.
  +
* Сервисы могут деплоится в любых сочетаниях - один, два, все сразу, первый и третий, только номер N ...
  +
* Для сервиса который выбран для деплоймента предоставлять выбор бранчи, для сервиса который не выбран - не предоставлять
  +
  +
  +
== Пример интерфейса ==
  +
* Слева - выбран один сервис и бранча для него, второй сервис не выбран и соответвенно выбор бранчи для него недоступен.
  +
* Справа - выбрано два сервиса и бранчи для обоих сервисов
  +
[[File:Screenshot_2022-07-12_at_11.53.46.png|400px]]
  +
[[File:Screenshot_2022-07-12_at_11.54.06.png|400px]]
  +
  +
== Как решается задача==
  +
Собственно задача сводится к тому что бы определить изменение какого параметра вызвало запуск скрипта
  +
<BR>
  +
Так как значение того параметра что вызвало запуск скрипта передается в скрипт (имя переменной совпадает с именем параметра и в примере это будет app1 или app2 или ... appN)
  +
<BR>
  +
то этим можно воспользоваться - создать список сервисов с информацией о них и их именами
  +
<PRE>
  +
List reposAndKeys = [
  +
[
  +
"name": "app1",
  +
"repo": "git@github.com:app2.git",
  +
"repoKeyId": "key1",
  +
],
  +
[
  +
"name": "app2",
  +
"repo": "git@github.com:app2.git",
  +
"repoKeyId": "key2",
  +
],
  +
...
  +
</PRE>
  +
Составить список какие имена могут быть доступны?
  +
<PRE>
  +
List applications = []
  +
reposAndKeys.each { repoAndKey ->
  +
applications.add(repoAndKey["name"])
  +
}
  +
</PRE>
  +
Проверить существование переменной (и ее значение) можно вот таким способом: если скрипт вызван изменением значения параметра app1 то переменная app1 будет доступна внутри кода а переменная app2 нет (это верно для любого appN)
  +
<PRE>
  +
applications.each { it ->
  +
try {
  +
// если it указывает на несуществующее значение то тут будет исключение
  +
println("${it} = ${this[ it ]}")
  +
  +
// Если чекбокс не выбран - то в значении переменной (имя переменной это имя параметра) будет пустая строка
  +
// тут нужно добаить другие проверки если планируется использовать другиме типы параметов
  +
if ("${this[ it ]}" == "") {
  +
isDisabled = true
  +
}
  +
triggeredByParameterNames.add("${it}")
  +
} catch(Exception Ex) {
  +
println("${it}")
  +
println(Ex)
  +
}
  +
}
  +
</PRE>
  +
Некоторые дополнительные вещи вроде обработки ситуаций когда не выбран не один appN но скрипт запущен или ошибочно сконфигурировано более чем 1 appN как параметр вызывающей пересчет сиска бранчей
  +
  +
= Пример кода =
  +
  +
==Описаниа в Jenkins Job Builder==
  +
{{#spoiler:show=Jenkins job builder definition|
  +
<PRE>
  +
- 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"
  +
  +
</PRE>
  +
}}
  +
  +
==Код branch selector ==
  +
{{#spoiler:show=pipelines/common/frontend/build/active-choice-scripts/branch-selector.groovy|
  +
<PRE>
  +
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)
  +
</PRE>
  +
}}

Текущая версия на 11:57, 12 июля 2022


Jenkins Active Choice

Есть плагин который позволяет динамически формировать значения параметров, например динамически подтягивать список веток в git (НО нельзя динамически создать параметры, и у меня не вышло сделать скрытыми параметры в зависимости от того что выбрано в другом параметре)

В чем сложность ?

В целом решается задача достаточно несложно - особенно когда есть одно приложение и один репозиторий, но ситуация другая когда приложений и репозиториев (а соответственно и ключей к ним) становится много
Это тоже не кажется сложным, просто использовать для каждого приложения свой скрипт где будет "зашит" свой адрес и ключ.
НО в этом случае получаем множество скриптов которые нужно будет синхронно обновлять и правильно вызывать
Хочется использовать ОДИН скрипт который единственный модифицировать при добавлении приложения.



Постановка задачи

  • Деплоймент приложения состоящего из множества сервисов: app1, app2, app3 ... appN. Число сервисов может увеличиваться.
  • Сервисы могут деплоится в любых сочетаниях - один, два, все сразу, первый и третий, только номер N ...
  • Для сервиса который выбран для деплоймента предоставлять выбор бранчи, для сервиса который не выбран - не предоставлять


Пример интерфейса

  • Слева - выбран один сервис и бранча для него, второй сервис не выбран и соответвенно выбор бранчи для него недоступен.
  • Справа - выбрано два сервиса и бранчи для обоих сервисов

Screenshot 2022-07-12 at 11.53.46.png Screenshot 2022-07-12 at 11.54.06.png

Как решается задача

Собственно задача сводится к тому что бы определить изменение какого параметра вызвало запуск скрипта
Так как значение того параметра что вызвало запуск скрипта передается в скрипт (имя переменной совпадает с именем параметра и в примере это будет 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

Код branch selector