JenkinsActiveChoice: различия между версиями
Sirmax (обсуждение | вклад) (Новая страница: «111») |
Sirmax (обсуждение | вклад) |
||
(не показаны 22 промежуточные версии этого же участника) | |||
Строка 1: | Строка 1: | ||
+ | [[Категория:Jenkins]] |
||
− | 111 |
||
+ | |||
+ | =Jenkins Active Choice= |
||
+ | Есть плагин который позволяет динамически формировать значения параметров, например динамически подтягивать список веток в 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 ...
- Для сервиса который выбран для деплоймента предоставлять выбор бранчи, для сервиса который не выбран - не предоставлять
Пример интерфейса
- Слева - выбран один сервис и бранча для него, второй сервис не выбран и соответвенно выбор бранчи для него недоступен.
- Справа - выбрано два сервиса и бранчи для обоих сервисов
Как решается задача
Собственно задача сводится к тому что бы определить изменение какого параметра вызвало запуск скрипта
Так как значение того параметра что вызвало запуск скрипта передается в скрипт (имя переменной совпадает с именем параметра и в примере это будет 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 как параметр вызывающей пересчет сиска бранчей