Архив автора: admin

Удаляем устаревшую feature branch в Kubernetes кластере

Привет! Feature branch (aka deploy preview, review app) — это когда деплоится не только master ветка, но и каждый pull request на уникальный URL. Можно проверить работает ли код в production-окружении, фичу можно показать другим программистам или продуктологам. Пока вы работаете в pull request’е, каждый новый commit текущий deploy для старого кода удаляется, а новый deploy для нового кода выкатывается. Вопросы могут возникнуть тогда, когда вы смерджили pull request в master ветку. Feature branch вам больше не нужна, но ресурсы Kubernetes все еще находятся в кластере.




Еще про feature branch’и




Один из подходов как сделать feature branch’и в Kubernetes — использовать namespace’ы. Если кратко, production конфигурации выглядит так:




kind: Namespace
apiVersion: v1
metadata:
  name: habr-back-end
...

kind: Deployment
apiVersion: apps/v1
metadata:
  namespace: habr-back-end
spec:
  replicas: 3
...




Для feature branch создается namespace c ее идентификатором (например, номер pull request’а) и каким-то префиксом/постфиксом (например, -pr-):




kind: Namespace
apiVersion: v1
metadata:
  name: habr-back-end-pr-17
...

kind: Deployment
apiVersion: apps/v1
metadata:
  namespace: habr-back-end-pr-17
spec:
  replicas: 1
...




В общем, я написал Kubernetes Operator (приложение, которое имеет доступ к ресурсам кластера), ссылка на проект на Github. Он удаляет namespace’ы, которые относятся к старым feature branch’ам. В Kubernetes, если удалить namespace, другие ресурсы в этом namespace также удаляются автоматически.




$ kubectl get pods --all-namespaces | grep -e "-pr-"
NAMESPACE            ... AGE
habr-back-end-pr-264 ... 4d8h
habr-back-end-pr-265 ... 5d7h




Про то как внедрить feature branch’и в кластер, можно почитать тут и тут.




Мотивация




Давайте посмотрим на типичный жизненный цикл pull request’a с непрерывной интеграцией (continuous integration):




  1. Пушим новый commit в ветку.



  2. На билде, запускаются линтеры и/или тесты.



  3. На лету формируются конфигурации Kubernetes pull request’a (например, в готовый шаблон подставляется его номер).



  4. С помощью kubectl apply конфигурации попадают в кластер (deploy).



  5. Pull request сливается в master ветку.




Пока вы работаете в pull request’е, каждый новый commit текущий deploy для старого кода удаляется, а новый deploy для нового кода выкатывается. Но когда pull request сливается в master ветку, будет билдится только master ветка. В итоге получается, что про pull request мы уже забыли, а его Kubernetes ресурсы все еще находятся в кластере.




Как использовать




Установить проект командой ниже:




$ kubectl apply -f https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/configs/production.yml




Создать файл со следующим содержанием и установить через kubectl apply -f:




apiVersion: feature-branch.dmytrostriletskyi.com/v1
kind: StaleFeatureBranch
metadata:
  name: stale-feature-branch
spec:
  namespaceSubstring: -pr-
  afterDaysWithoutDeploy: 3




Параметр namespaceSubstring нужен, чтобы отфильтровать namespace’ы для pull request’ов от других namespace’ов. Например, если в кластере есть следующие namespace’ы: habr-back-endhabr-front-endhabr-back-end-pr-17habr-back-end-pr-33, тогда кандидатами на удаление будут habr-back-end-pr-17habr-back-end-pr-33.




Параметр afterDaysWithoutDeploy нужен чтобы, удалять старые namespace’ы. Например, если namespace создан 3 дня 1 час назад, а в параметре указано 3 дня, этот namespace будет удален. Работает и в обратную сторону, если namespace создан 2 дня 23 часа назад, а в параметре указано 3 дня, этот namespace не будет удален.




Есть еще один параметр, он отвечает за то как часто сканировать все namespace’ы и проверять на дни без deploy’я — checkEveryMinutes. По умолчанию он равен 30 минутам.




Как это работает




На практике, понадобится:




  1. Docker для работы в изолированном окружении.



  2. Minikube поднимет Kubernetes кластер локально.



  3. kubectl — интерфейс командной строки для управления кластером.




Поднимаем Kubernetes кластер локально:




$ minikube start --vm-driver=docker
minikube v1.11.0 on Darwin 10.15.5
Using the docker driver based on existing profile.
Starting control plane node minikube in cluster minikube.




Указываем kubectl использовать локальный кластер по умолчанию:




$ kubectl config use-context minikube
Switched to context "minikube".




Скачиваем конфигурации для production-среды:




$ curl https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/configs/production.yml > stale-feature-branch-production-configs.yml




Так как production конфигурации настроены проверять старые namespace’ы, а в нашем ново поднятом кластере их нет, заменим переменную окружения IS_DEBUG на true. При таком значении параметр afterDaysWithoutDeploy не учитывается и namespace’ы не проверяются на дни без deploy’я, только на вхождение подстроки (-pr-).




Если вы на Linux:




$ sed -i 's|false|true|g' stale-feature-branch-production-configs.yml




Если вы на macOS:




$ sed -i "" 's|false|true|g' stale-feature-branch-production-configs.yml




Устанавливаем проект:




$ kubectl apply -f stale-feature-branch-production-configs.yml




Проверяем, что в кластере появился ресурс StaleFeatureBranch:




$ kubectl api-resources | grep stalefeaturebranches
NAME                 ... APIGROUP                             ... KIND
stalefeaturebranches ... feature-branch.dmytrostriletskyi.com ... StaleFeatureBranch




Проверяем, что в кластере появился оператор:




$ kubectl get pods --namespace stale-feature-branch-operator
NAME                                           ... STATUS  ... AGE
stale-feature-branch-operator-6bfbfd4df8-m7sch ... Running ... 38s




Если заглянуть в его логи, он готов обрабатывать ресурсы StaleFeatureBranch:




$ kubectl logs stale-feature-branch-operator-6bfbfd4df8-m7sch -n stale-feature-branch-operator
... "msg":"Operator Version: 0.0.1"}
...
... "msg":"Starting EventSource", ... , "source":"kind source: /, Kind="}
... "msg":"Starting Controller", ...}
... "msg":"Starting workers", ..., "worker count":1}




Устанавливаем готовые fixtures (готовые конфигурации для моделирования ресурсов кластера) для ресурса StaleFeatureBranch:




$ kubectl apply -f https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/fixtures/stale-feature-branch.yml




В конфигурациях указано искать namespace’ы с подстрокой -pr- раз в 1 минуту.:




apiVersion: feature-branch.dmytrostriletskyi.com/v1
kind: StaleFeatureBranch
metadata:
  name: stale-feature-branch
spec:
  namespaceSubstring: -pr-
  afterDaysWithoutDeploy: 1 
  checkEveryMinutes: 1




Оператор отреагировал и готов проверять namespace’ы:




$ kubectl logs stale-feature-branch-operator-6bfbfd4df8-m7sch -n stale-feature-branch-operator
... "msg":"Stale feature branch is being processing.","namespaceSubstring":"-pr-","afterDaysWithoutDeploy":1,"checkEveryMinutes":1,"isDebug":"true"}




Устанавливаем fixtures, содержащие два namespace’а (project-pr-1project-pr-2) и их deploymentsservicesingress, и так далее:




$ kubectl apply -f https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/fixtures/first-feature-branch.yml -f https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/fixtures/second-feature-branch.yml
...
namespace/project-pr-1 created
deployment.apps/project-pr-1 created
service/project-pr-1 created
horizontalpodautoscaler.autoscaling/project-pr-1 created
secret/project-pr-1 created
configmap/project-pr-1 created
ingress.extensions/project-pr-1 created
namespace/project-pr-2 created
deployment.apps/project-pr-2 created
service/project-pr-2 created
horizontalpodautoscaler.autoscaling/project-pr-2 created
secret/project-pr-2 created
configmap/project-pr-2 created
ingress.extensions/project-pr-2 created




Проверяем, что все ресурсы выше успешно созданы:




$ kubectl get namespace,pods,deployment,service,horizontalpodautoscaler,configmap,ingress -n project-pr-1 && kubectl get namespace,pods,deployment,service,horizontalpodautoscaler,configmap,ingress -n project-pr-2
...
NAME                              ... READY ... STATUS  ... AGE
pod/project-pr-1-848d5fdff6-rpmzw ... 1/1   ... Running ... 67s

NAME                         ... READY ... AVAILABLE ... AGE
deployment.apps/project-pr-1 ... 1/1   ... 1         ... 67s
...




Так как мы включили debug, namespace’ы project-pr-1 и project-pr-2, следовательно и все остальные ресурсы, должны будут сразу удалиться не учитывая параметр afterDaysWithoutDeploy. В логах оператора это видно:




$ kubectl logs stale-feature-branch-operator-6bfbfd4df8-m7sch -n stale-feature-branch-operator
... "msg":"Namespace should be deleted due to debug mode is enabled.","namespaceName":"project-pr-1"}
... "msg":"Namespace is being processing.","namespaceName":"project-pr-1","namespaceCreationTimestamp":"2020-06-16 18:43:58 +0300 EEST"}
... "msg":"Namespace has been deleted.","namespaceName":"project-pr-1"}
... "msg":"Namespace should be deleted due to debug mode is enabled.","namespaceName":"project-pr-2"}
... "msg":"Namespace is being processing.","namespaceName":"project-pr-2","namespaceCreationTimestamp":"2020-06-16 18:43:58 +0300 EEST"}
... "msg":"Namespace has been deleted.","namespaceName":"project-pr-2"}




Если проверить наличие ресурсов, они будут в статусе Terminating (процесс удаления) или уже удалены (вывод команды пуст).




$ kubectl get namespace,pods,deployment,service,horizontalpodautoscaler,configmap,ingress -n project-pr-1 && kubectl get namespace,pods,deployment,service,horizontalpodautoscaler,configmap,ingress -n project-pr-2
...




Можете повторить процесс создания fixtures несколько раз и убедиться, что они будут удалены в течение минуты.




Альтернативы




Что можно сделать вместо оператора, который работает в кластере? Подходов несколько, все они неидеальны (и их недостатки субъективны), и каждый сам решает что лучше всего подойдет на конкретном проекте:




  1. Удалять feature branch во время билда непрерывной интеграции master ветки.

    • Для этого надо знать какой pull request относится к commit’у, который билдится. Так как feature branch namespace содержит в себе идентификатор pull request’a — его номер, или название ветки, идентификатор всегда придется указывать в commit’e.



    • Билды master веток фейлятся. Например, у вас следующие этапы: скачать проект, запустить тесты, собрать проект, сделать релиз, отправить уведомления, очистить feature branch последнего pull request’a. Если билд сфейлится на отправке уведомления, вам придется удалять все ресурсы в кластере руками.



    • Без должного контекста, удаление feature branch’и в master билде неочевидно.




  2. Использование webhook’ов (пример).

    • Возможно, это не ваш подход. Например, в Jenkins, только один вид пайплайна поддерживает возможность сохранять его конфигурации в исходном коде. При использовании webhook’ов нужно написать свой скрипт для их обработки. Этот скрипт придется размещать в интерфейсе Jenkins’а, что трудно поддерживать.




  3. Написать Cronjob и добавить Kubernetes кластер.

    • Затрата времени на написание и поддержку.



    • Оператор уже работает в подобном стиле, задокументирован и поддерживается.




Спасибо за внимание к статье. Ссылка на проект на Github.




Источник: https://habr.com/ru/post/508534/



2023-01-03T00:32:43
DevOps

Jenkins as a code. Часть 4

Возникла необходимость разграничения прав доступа пользователей на Jenkins-сервере — давайте разберемся, как можно это сделать без использования webUI!




В моем случае нужно разделить пользователей на две группы — администраторов (с полным уровнем доступа) и обычных пользователей (есть доступ только на просмотр списка заданий, запуск и отмену запущенных задач). Для решения этой задачи отлично подходит плагин Role-based Authorization Strategy. Главной особенностью является наличие в данном плагине двух предустановленных групп пользователей:




  • Anonymous — незалогиненные пользователи;



  • authenticated — залогиненные пользователи.




Использование группы authenticated даст возможность не перечислять всех пользователей нашего jenkins-сервера в явном виде.




Устанавливать данный плагин мы будем при старте jenkins-сервера с помощью инит-скрипта, как описано в этой статье. Для этого в скрипте 00-install-plugins.groovy список устанавливаемых плагинов приводим к следующему виду:




...
Set<String> plugins_to_install = [
    "github-pullrequest",
    "google-login",
    "workflow-aggregator",
    "htmlpublisher",
    "locale",
    "role-strategy"
]
...




Теперь создадим еще один groovy-скрипт (назовем его 06-role-based-auth.groovy) со следующим содержанием:




import hudson.*
import hudson.model.*
import hudson.security.*
import jenkins.*
import jenkins.model.*
import java.util.*
import com.michelin.cio.hudson.plugins.rolestrategy.*
import java.lang.reflect.*

// Roles
def globalRoleRead = "builder"
def globalRoleAdmin = "admin"

// Users and Groups
def access = [
    admins: [
            "admin@example.com",
            "devops@example.com",
            "ealebed@example.com"
    ],
    builders: ["authenticated"],
]

def instance = Jenkins.getInstance()
def currentAuthenticationStrategy = Hudson.instance.getAuthorizationStrategy()

Thread.start {
    sleep 15000
    if (currentAuthenticationStrategy instanceof RoleBasedAuthorizationStrategy) {
        println "Role based authorisation already enabled."
        println "Exiting script..."
        return
    } else {
        println "Enabling role based authorisation strategy..."
    }

    // Set new authentication strategy
    RoleBasedAuthorizationStrategy roleBasedAuthenticationStrategy = new RoleBasedAuthorizationStrategy()
    instance.setAuthorizationStrategy(roleBasedAuthenticationStrategy)

    Constructor[] constrs = Role.class.getConstructors()
    for (Constructor<?> c : constrs) {
        c.setAccessible(true)
    }

    // Make the method assignRole accessible
    Method assignRoleMethod = RoleBasedAuthorizationStrategy.class.getDeclaredMethod("assignRole", String.class, Role.class, String.class)
    assignRoleMethod.setAccessible(true)

    // Create admin set of permissions
    Set<Permission> adminPermissions = new HashSet<Permission>()
    adminPermissions.add(Permission.fromId("hudson.model.Hudson.Administer"))
    adminPermissions.add(Permission.fromId("hudson.model.Hudson.Read"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.View"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.ManageDomains"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Create"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Update"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Read"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Read"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Discover"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Workspace"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Move"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Build"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Cancel"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Connect"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Build"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Provision"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Disconnect"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Update"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Replay"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Delete"))
    adminPermissions.add(Permission.fromId("hudson.scm.SCM.Tag"))

    // Create permissions for authenticated users
    Set<Permission> authenticatedPermissions = new HashSet<Permission>()
    authenticatedPermissions.add(Permission.fromId("hudson.model.Hudson.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.View.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Build"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Cancel"))

    // Create the admin role
    Role adminRole = new Role(globalRoleAdmin, adminPermissions)
    roleBasedAuthenticationStrategy.addRole(RoleBasedAuthorizationStrategy.GLOBAL, adminRole)

    // Create the builder role
    Role builderRole = new Role(globalRoleRead, authenticatedPermissions)
    roleBasedAuthenticationStrategy.addRole(RoleBasedAuthorizationStrategy.GLOBAL, builderRole)

    // Assign the admin role
    access.admins.each { l ->
        println("Granting admin permissions to ${l}")
        roleBasedAuthenticationStrategy.assignRole(RoleBasedAuthorizationStrategy.GLOBAL, adminRole, l)
    }

    access.builders.each { l ->
        println("Granting builder permissions to ${l}")
        roleBasedAuthenticationStrategy.assignRole(RoleBasedAuthorizationStrategy.GLOBAL, builderRole, l)
    }

    // Save the state
    println "Saving changes."
    instance.save()
}




С помощью данного скрипта создаются две роли — builder и admin, которым устанавливаются разные наборы прав доступа. Далее всем пользователям, перечисленным в access.admins, назначается роль admin, а группе authenticated (находится в access.builders) назначается роль builder.




Источник: https://ealebed.github.io/posts/2018/jenkins-as-a-code-часть-4/



2023-01-03T00:16:15
DevOps

Jenkins as a code. Часть 3

В данной статье цикла “Jenkins as a code” рассмотрим самый интересный (и полезный) пример — автоматическое создание задач (job) при запуске сервиса. Давайте разберемся!




Создавать задания (jobs) в Jenkins можно несколькими способами — традиционно (через webUI), через REST API или “подкладывая” файлы .xml.import в ${JENKINS_HOME}.




Но традиционный способ настройки занимает слишком много времени, использование API невероятно неудобно (см. пример ниже), а создание файлов с расширением .xml.import для каждой задачи и рестарт сервиса Jenkins после их добавления даже звучит несерьезно.




Чем плох REST API:




  • сначала необходимо получить описание уже существующей задачи в формате .xml. Например, это можно сделать используя API:




curl -X GET -u username:API_TOKEN 'http://JENKINS_HOST/job/MY_JOB_NAME/config.xml' -o config.xml




или




curl -u username:password "http://JENKINS_HOST/job/MY_JOB_NAME/config.xml" > config.xml




  • потом полученный xml-файл нужно отредактировать под свои нужды;



  • после редактирования можно создать новую задачу “запостив” этот файл через API, например:




curl -u username:password -s -XPOST 'http://JENKINS_HOST/createItem?name=newJobName' --data-binary config.xml -H "Content-Type:text/xml"




Ах да, если вдруг столкнетесь с ошибкой:




Error 403 No valid crumb was included in the request




то придется сделать на одно действие больше:




CRUMB=$(curl -s 'http://JENKINS_HOST/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' -u username:password)//crumb)' -u us




curl -u username:password -s -XPOST 'http://JENKINS_HOST/createItem?name=newJobName' --data-binary config.xml -H "$CRUMB" -H "Content-Type:text/xml"




Вообщем, так себе вариант, особенно если у вас 150-200 заданий…




Вспоминаем о возможностях настройки экземпляра Jenkins еще в момент старта с помощью хуков, которые мы рассматривали в первой  и второй статьях данного цикла. Идея — хранить в отдельном файле определенного формата (.json или .xml) минимально необходимый набор параметров (ссылка на git-репозиторий, название ветки, путь к Jenkinsfile и т. д.) для создания задачи. При запуске Jenkins groovy-скрипт берет из данного файла параметры и создает задачи, в качестве дополнительного бонуса — сразу распределяет эти задачи по представлениям (views).




Создаем файл job_list.yaml с набором параметров для задач, в моем случае выглядящий так:




dev:
  1st-project:
    jobDisabled:          false
    enableGithubPolling:  false
    daysBuildsKeep:       '1'
    numberBuildsKeep:     '5'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master, */test'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'
  2nd-project:
    jobDisabled:          false
    enableGithubPolling:  true
    daysBuildsKeep:       '2'
    numberBuildsKeep:     '10'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'
stage:
  3rd-project:
    jobDisabled:          false
    enableGithubPolling:  true
    daysBuildsKeep:       '3'
    numberBuildsKeep:     '15'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'dir/Jenkinsfile'
test:
  4th-project:
    jobDisabled:          true
    enableGithubPolling:  true
    daysBuildsKeep:       '4'
    numberBuildsKeep:     '20'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'




Примечание. Если планируется переезд со старого экземпляра Jenkins на новый, то можно легко написать скрипт, который возьмет нужные параметры на старом сервере и сформирует yaml-файл в нужном формате.




Скрипт, который должен “разобрать” данный файл и создать задачи у меня называется 05-create-jobs.groovy и выглядит следующим образом:




@Grab(group='org.yaml', module='snakeyaml', version='1.18')
import jenkins.model.*
import hudson.model.*
import hudson.tasks.LogRotator
import hudson.plugins.git.*
import org.jenkinsci.plugins.workflow.job.WorkflowJob
import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition
import com.coravy.hudson.plugins.github.*
import com.cloudbees.jenkins.GitHubPushTrigger
import org.yaml.snakeyaml.Yaml

Jenkins jenkins = Jenkins.getInstance()

def pluginParameter = "workflow-aggregator github-pullrequest"
def plugins         = pluginParameter.split()
def pm              = jenkins.getPluginManager()
def installed       = false

plugins.each {
    if (!pm.getPlugin(it)) {
        println("Plugin ${it} not installed, skip creating jobs!")
    } else {
        installed = true
    }
}

if(installed) {
    def listExistingJob     = jenkins.items.collect { it.name }
    def listExistingViews   = jenkins.views.collect { it.name }
    def jobParameters       = new Yaml().load(new FileReader('/temp/job_list.yaml'))

    for (view in jobParameters) {
        if (jobParameters.any { listExistingViews.contains(view.key) }) {
            println("--- View ${view.key} already exist, skip")
        } else {
            println("--- Create new view ${view.key}")
            jenkins.addView(new ListView(view.key))
        }
        for (item in view.value) {
            if (view.value.any { listExistingJob.contains(item.key) }) {
                println("--- Job ${item.key} already exist, skip")
                continue
            } else {
                println("--- Create new job ${item.key}")

                def jobName             = item.key
                def jobDisabled         = view.value.get(item.key).getAt("jobDisabled")
                def enableGithubPolling = view.value.get(item.key).getAt("enableGithubPolling")
                def daysBuildsKeep      = view.value.get(item.key).getAt("daysBuildsKeep")
                def numberBuildsKeep    = view.value.get(item.key).getAt("numberBuildsKeep")
                def githubRepoUrl       = view.value.get(item.key).getAt("githubRepoUrl")
                def githubRepo          = view.value.get(item.key).getAt("githubRepo")
                def githubBranch        = view.value.get(item.key).getAt("githubBranch")
                def credentialId        = view.value.get(item.key).getAt("credentialId")
                def jenkinsfilePath     = view.value.get(item.key).getAt("jenkinsfilePath")

                def githubBranchList = githubBranch.split(', ')
                def branchConfig = new ArrayList<BranchSpec>()
                for (branch in githubBranchList) {
                    if( branch != null )
                    {
                        branchConfig.add(new BranchSpec(branch))
                    }
                    else
                    {
                        branchConfig.add(new BranchSpec("*/master"))
                    }
                }
                def userConfig          = [new UserRemoteConfig(githubRepo, null, null, credentialId)]
                def scm                 = new GitSCM(userConfig, branchConfig, false, [], null, null, null)
                def flowDefinition      = new CpsScmFlowDefinition(scm, jenkinsfilePath)

                flowDefinition.setLightweight(true)

                def job = new WorkflowJob(jenkins, jobName)
                job.definition = flowDefinition
                job.setConcurrentBuild(false)
                job.setDisabled(jobDisabled)
                job.addProperty(new BuildDiscarderProperty(new LogRotator(daysBuildsKeep, numberBuildsKeep, null, null)))
                job.addProperty(new GithubProjectProperty(githubRepoUrl))

                if (true == enableGithubPolling) {
                    job.addTrigger(new GitHubPushTrigger())
                }

                jenkins.save()
                jenkins.reload()
                hudson.model.Hudson.instance.getView(view.key).doAddJobToView(jobName)
            }
        }
    }
}




Сначала мы проверяем, установлены ли необходимые плагины (без них создать представление / задачу не получится). Далее получаем список уже существующих представлений и задач (может так случиться, что сервер с Jenkins будет перезагружен) — нам совершенно нет нужды пересоздавать задачи при каждом старте Jenkins. Только если нужного нам представления (view) нет — оно будет создано, после чего создаются добавляются в представление связанные с ним задачи.




Остается только позаботиться о том, чтобы файл с параметрами задач появился в /temp/job_list.yaml (или другом удобном для вас месте), а скрипт 05-create-jobs.groovy — в каталоге ${JENKINS_HOME}/init.groovy.d/.




В заключительной статье цикла рассмотрим еще один важный вопрос разграничения прав доступа пользователей.



2023-01-03T00:15:17
DevOps

Jenkins as a code. Часть 2

В первой статье цикла “Jenkins as a code” мы разобрали несколько примеров настройки экземпляра Jenkins под собственные нужды, в данной статье рассмотрим автоматическую настройку общих библиотек (Shared Libraries) — давайте разберемся!




Ранее мы уже подробно рассматривали значение общих библиотек для большого количества однотипных проектов, поэтому повторяться не будем. Наша цель — сделать так, чтобы после старта Jenkins общая библиотека уже была подключена и настроена — это избавит от необходимости настройки через UI.




Как и в предыдущей статье, нам на помощь приходят хуки (groovy-скрипты) из каталога ${JENKINS_HOME}/init.groovy.d/. Создадим новый файл 04-global-pipeline-library.groovy следующего содержания:




import jenkins.model.Jenkins
import jenkins.plugins.git.GitSCMSource
import jenkins.plugins.git.traits.BranchDiscoveryTrait
import org.jenkinsci.plugins.workflow.libs.GlobalLibraries
import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration
import org.jenkinsci.plugins.workflow.libs.SCMSourceRetriever

List libraries = [] as ArrayList

def instance = Jenkins.getInstance()

println("--- Configuring global library getting")
def githubRepo      = 'https://github.com/ealebed/jenkins-shared-libs.git'
def libraryName     = 'jenkins-shared-libs'
def githubBranch    = 'master'
def credentialsId   = ''

def scm             = new GitSCMSource(githubRepo)
scm.credentialsId   = credentialsId
scm.traits          = [new BranchDiscoveryTrait()]
def retriever       = new SCMSourceRetriever(scm)

def library         = new LibraryConfiguration(libraryName, retriever)
library.defaultVersion          = githubBranch
library.implicit                = true
library.allowVersionOverride    = true
library.includeInChangesets     = true

libraries << library

def global_settings = instance.getExtensionList(GlobalLibraries.class)[0]
global_settings.libraries = libraries
global_settings.save()




Интересующие нас настройки задаются с помощью четырех параметров — githubRepolibraryNamegithubBranch и credentialsId.




Здесь:




  • githubRepo — ссылка на репозиторий с общими библиотеками в системе контроля версий (может быть как https, так и git);



  • libraryName — имя подключаемой общей библиотеки. Именно это имя будет использоваться в пайплайнах (Jenkinsfile) в формате @Library('jenkins-shared-libs@master') _;



  • githubBranch — ветка в git-репозитории с общими библиотеками (Она же “версия”). Также используется в пайплайнах (Jenkinsfile);



  • credentialsId — креденшелы для подключения к git-репозиторию. Значение может быть пустым — помните скрипт 03-user-service.groovy из первой части цикла, которым мы настраивали доступ к системе контроля версий?




В зависимости от выбранного инструмента развертывания Jenkins, нужно добиться появления данного скрипта в каталоге ${JENKINS_HOME}/init.groovy.d/ — так он будет выполнен при запуске сервиса.




Источник: https://ealebed.github.io/posts/2018/jenkins-as-a-code-часть-2/



2023-01-03T00:14:09
DevOps

Jenkins as a code. Часть 1

Идея “инфраструктура как код” далеко не нова и широко используется в повседневной жизни большинством компаний. В серии статей “Jenkins as a code” предлагаю разобраться с автоматическим развертыванием и настройкой сервера Jenkins!




Казалось бы, зачем эти статьи, если можно взять готовую роль jenkins для системы управления конфигурациями Ansible или кукбук jenkins для chef, или даже воспользоваться готовым docker-образом?




Развертывание базовой конфигурации — это действительно несложный процесс (мы его рассматривать не будем, тут каждый волен выбирать свои инструменты), поэтому остановимся подробнее именно на настройке Jenkins под собственные нужды (данная статья), автоматической настройке общих библиотек (вторая часть) и импорте задач (третья часть).




Для кастомизации и тонкой настройки вашего экземпляра Jenkins разработчики предлагают использовать хуков (groovy-скриптов), которые нужно размещать в каталоге ${JENKINS_HOME}/init.groovy.d/.




В зависимости от выбранного инструмента развертывания, способ, которым скрипты попадут в нужный каталог будет отличаться. Например, при использовании docker-образа, самым простым будет поместить нужные скрипты в каталог /usr/share/jenkins/ref/init.groovy.d/:




FROM jenkins/jenkins:lts
COPY custom.groovy /usr/share/jenkins/ref/init.groovy.d/custom.groovy




При старте docker-контейнера все, что находится в каталоге /usr/share/jenkins/ref/ копируется в каталог ${JENKINS_HOME} (следовательно, каталог init.groovy.d со всем содержимым будет скопирован в нужное место).




Стоит отметить, что скрипты из каталоге ${JENKINS_HOME}/init.groovy.d/ запускаются при старте Jenkins и выполняются в алфавитном порядке — это очень важный момент, если нужно соблюдать последовательность запуска.




Чаще всего с помощью хуков в Jenkins устанавливают плагины, выполняют глобальную настройку, включают/выключают опции безопасности, добавляют ключи доступа к системе хранения версиями.




В моем случае для тонкой настройки экземпляра Jenkins используется несколько скриптов. Для соблюдения порядка запуска в начале имени каждого скрипта присутствуют цифры.




Скрипт 00-install-plugins.groovy выполняет установку необходимых плагинов с зависимостями и выглядит следующим образом:




/*
    Install required plugins and their dependencies.
*/
import jenkins.model.*
import hudson.model.*
import org.jenkinsci.plugins.*
import hudson.model.UpdateSite
import hudson.PluginWrapper

Set<String> plugins_to_install = [
    "github-pullrequest",
    "google-login",
    "workflow-aggregator",
    "htmlpublisher",
    "locale"
]

Boolean hasConfigBeenUpdated = false
UpdateSite updateSite = Jenkins.getInstance().getUpdateCenter().getById('default')
List<PluginWrapper> plugins = Jenkins.instance.pluginManager.getPlugins()

def install_plugin(shortName, UpdateSite updateSite) {
    println "Installing ${shortName} plugin."
    UpdateSite.Plugin plugin = updateSite.getPlugin(shortName)
    Throwable error = plugin.deploy(false).get().getError()
    if(error != null) {
        println "ERROR installing ${shortName}, ${error}"
    }
    null
}

// Check the update site(s) for latest plugins
println 'Checking plugin updates via Plugin Manager.'
Jenkins.instance.pluginManager.doCheckUpdatesServer()

// Any plugins need updating?
Set<String> plugins_to_update = []
plugins.each {
    if(it.hasUpdate()) {
        plugins_to_update << it.getShortName()
    }
}

if(plugins_to_update.size() > 0) {
    println "Updating plugins..."
    plugins_to_update.each {
        install_plugin(it, updateSite)
    }
    println "Done updating plugins."
    hasConfigBeenUpdated = true
}

// Get a list of installed plugins
Set<String> installed_plugins = []
plugins.each {
    installed_plugins << it.getShortName()
}

// Check to see if there are missing plugins to install
Set<String> missing_plugins = plugins_to_install - installed_plugins
if(missing_plugins.size() > 0) {
    println "Install missing plugins..."
    missing_plugins.each {
        install_plugin(it, updateSite)
    }
    println "Done installing missing plugins."
    hasConfigBeenUpdated = true
}

if(hasConfigBeenUpdated) {
    println "Saving Jenkins configuration to disk."
    Jenkins.instance.save()
    Jenkins.instance.restart()
} else {
    println "Jenkins up-to-date. Nothing to do."
}




Вторым по счету запускается скрипт 01-global-settings.groovy, устанавливающий количество исполнителей, локаль, глобальные настройки для системы контроля версий и протоколы взаимодействия:




import jenkins.model.*
import org.jenkinsci.plugins.*
import hudson.security.csrf.DefaultCrumbIssuer
import hudson.plugins.locale.PluginImpl

def instance = Jenkins.getInstance()

println("--- Configuring global getting")
instance.setNumExecutors(5)
instance.setCrumbIssuer(new DefaultCrumbIssuer(true))
instance.setNoUsageStatistics(true)
instance.save()

println("--- Configuring locale")
PluginImpl localePlugin = (PluginImpl)instance.getPlugin("locale")
localePlugin.systemLocale = "en_US"
localePlugin.@ignoreAcceptLanguage=true

println("--- Configuring git global options")
def desc = instance.getDescriptor("hudson.plugins.git.GitSCM")
desc.setGlobalConfigName("jenkins")
desc.setGlobalConfigEmail("jenkins@example.com")
desc.save()

println("--- Configuring protocols")
Set<String> agentProtocolsList = ['JNLP4-connect', 'Ping']
if(!instance.getAgentProtocols().equals(agentProtocolsList)) {
    instance.setAgentProtocols(agentProtocolsList)
    println "Agent Protocols have changed.  Setting: ${agentProtocolsList}"
    instance.save()
}
else {
    println "Nothing changed.  Agent Protocols already configured: ${instance.getAgentProtocols()}"
}




Следующим будет выполнен скрипт с именем 02-disable-cli.groovy (как несложно догадаться, отключающий CLI):




import jenkins.*
import jenkins.model.*
import hudson.model.*
import java.util.logging.Logger
import org.jenkinsci.main.modules.sshd.*
Logger logger = Logger.getLogger("")

// Disable CLI access over TCP listener (separate port)
def p = AgentProtocol.all()
p.each { x ->
    if (x.name?.contains("CLI")) {
        logger.info("Removing protocol ${x.name}")
        p.remove(x)
    }
}

// Disable CLI access over /cli URL
def removal = { lst ->
    lst.each { x ->
        if (x.getClass().name.contains("CLIAction")) {
            logger.info("Removing extension ${x.getClass().name}")
            lst.remove(x)
        }
    }
}

def j = Jenkins.instance
removal(j.getExtensionList(RootAction.class))
removal(j.actions)

// Disable CLI over Remoting
jenkins.CLI.get().setEnabled(false)

// Allow SSH connections
def sshdExtension = Jenkins.instance.getExtensionList(SSHD.class)[0]
sshdExtension.setPort(22222)
sshdExtension.save()

// Configure Slave-to-Master Access Control
// https://wiki.jenkins-ci.org/display/JENKINS/Slave+To+Master+Access+Control

def rule = Jenkins.instance.getExtensionList(jenkins.security.s2m.MasterKillSwitchConfiguration.class)[0].rule
if(!rule.getMasterKillSwitch()) {
    rule.setMasterKillSwitch(true);
    logger.info('Disabled agent -> master security for cobertura.');
}
else {
    logger.info('Nothing changed.  Agent -> master security already disabled.');
}

// Do not annoy with Slave-to-Master Access Control warning
Jenkins.instance.getExtensionList(jenkins.security.s2m.MasterKillSwitchWarning.class)[0].disable(true);
Jenkins.instance.save()




И, наконец, скрипт 03-user-service.groovy создает пользователя и добавляет ему ssh-ключ для доступа к системе контроля версий:




public_key = 'ssh-rsa AAAAB3N....TJChv jenkins'
user = hudson.model.User.get('service')
user.setFullName('Service User')
keys = new org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl(public_key)
user.addProperty(keys)
user.save()




На этом с настройкой экземпляра Jenkins под собственные нужды все, в следующей статье рассмотрим автоматическую настройку общих библиотек (Shared Libraries) при запуске Jenkins.




Источник: https://ealebed.github.io/posts/2018/jenkins-as-a-code-часть-1/



2023-01-03T00:12:36
DevOps

Jenkins: использование shared libraries

При использовании Jenkins в компании с большим количеством проектов, рано или поздно вы заметите, что ваши описания пайплайнов (Pipeline) имеют много общего. И, возможно, вам захочется избавиться от избыточности и следовать принципу DRY (Don’t Repeat Yourself) — давайте разберемся!




Помимо принципа DRY, важна также возможность внести изменения в код пайплайна один раз и автоматически использовать обновленный пайплайн в 50-100 других проектах.




Для этой цели как нельзя лучше подходят Shared Libraries — общие библиотеки, которые могут быть определены в отдельном репозитории системы управления версиями и загружены в описании пайплайна.




Структура каталогов в репозитории общих библиотек должна выглядеть следующим образом:




+- src                     # Source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- bar.json    # static helper data for org.foo.Bar




Примечание. В данной статье мы будем использовать только каталоги vars и resources.




Для нас наибольший интерес представляет директория vars — в ней можно разместить глобальные функции и переменные, доступные в пайплайнах. Согласно документации, имена файлов должны быть в camelCase формате (без дефисов/подчеркиваний и т.д. — это важно), и иметь расширение .groovy (именно эти файлы нас и интересуют) или .txt (для документации).




В каталоге resources можно разместить любые другие (не Java) файлы (например, .yaml или .json), которые будут загружаться в описании пайплайна с помощью шага libraryResource.




Итак, рассмотрим несколько примеров. Допустим, у нас есть простенький проект, в котором описание пайплайна (файл Jenkinsfile) выглядит так:




pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Build') {
      steps {
        script {
          sh '''
            sudo docker version
            sudo docker build -t ealebed/hellonode:latest .
            sudo docker image ls
          '''
        }
      }
    }
  }
}




Кроме описания пайплайна в репозитории находится еще два файла — Dockerfile:




FROM node:6.9
COPY server.js .
EXPOSE 8080
CMD node server.js




и файл server.js следующего содержания:




var http = require('http');
var handleRequest = function(request, response) {
  response.writeHead(200);
  response.end("Hello World!");
}
var www = http.createServer(handleRequest);
www.listen(8080);




В первой итерации выделим в отдельные функции части пайплайна, которые можно будет использовать в других проектах. Для этого:




  • создаем отдельный git-репозиторий для наших общих библиотек;



  • в репозитории создаем каталог vars;



  • в каталоге vars размещаем скрипт dockerCmd.groovy.




Содержимое dockerCmd.groovy:




def call(args) {
  assert args != null
  sh(script: "sudo docker ${args}")
}




Настраиваем использование Shared Libraries на Jenkins (пример с картинками). Теперь в нашем проекте пайплайн можно переписать так:




@Library('jenkins-shared-libs@master') _

pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Build') {
      steps {
        dockerCmd 'version'
        dockerCmd 'build -t ealebed/hellonode:latest .'
        dockerCmd 'image ls'
      }
    }
  }
}




Продолжаем. Во второй итерации избавимся от необходимости хранить Dockerfile в репозитории проекта. Для этого в git-репозитории с общими библиотеками создаем каталог resources и переносим в него Dockerfile из основного проекта. Далее, в каталоге vars размещаем скрипт createDockerfile.groovy следующего содержания:




def call() {
  def file = libraryResource 'Dockerfile'
  writeFile file: 'Dockerfile', text: file
}




Описание пайплайна в основном проекте изменяем на следующее:




@Library('jenkins-shared-libs@master') _

pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Get Dockerfile') {
      steps {
        createDockerfile()
      }
    }
    stage('Build') {
      steps {
        dockerCmd 'version'
        dockerCmd 'build -t ealebed/hellonode:latest .'
        dockerCmd 'image ls'
      }
    }
  }
}




Но что, если мы хотим использовать в других проектах не только отдельные функции, а весь пайплайн целиком? Нет ничего проще!




В git-репозитории с общими библиотеками в каталоге vars создаем скрипт allPipeline.groovy следующего содержания:




def call(body) {
  def pipelineParams= [:]
  body.resolveStrategy = Closure.DELEGATE_FIRST
  body.delegate = pipelineParams
  body()

  pipeline {
    agent any

    stages {
      stage('Checkout') {
        steps {
          checkout scm
        }
      }
      stage('Get Dockerfile') {
        steps {
          script {
            def tmpFile = libraryResource 'Dockerfile'
            writeFile file: 'Dockerfile', text: tmpFile
          }
        }
      }
      stage('Build') {
        steps {
          script {
            sh '''
              docker version
              docker build -t ealebed/hellonode:latest .
              docker image ls
            '''
          }
        }
      }
    }
  }
}




Теперь, в содержимое файла Jenkinsfile (описание пайплайна) в основном репозитории проекта невероятно упрощается:




@Library('jenkins-shared-libs@master') _

allPipeline {}




Больше интересных примеров можно найти в официальной документации, а также здесьздесь и здесь.




Don’t Repeat Yourself!




Источник: https://ealebed.github.io/posts/2018/jenkins-использование-shared-libraries/



2023-01-03T00:10:25
DevOps