Skip to content

Instantly share code, notes, and snippets.

@f0y
Last active June 28, 2020 12:29
Show Gist options
  • Save f0y/bf7ff6570699cc368613c0dd60cd9c69 to your computer and use it in GitHub Desktop.
Save f0y/bf7ff6570699cc368613c0dd60cd9c69 to your computer and use it in GitHub Desktop.
Динамическая генерация сборок Jenkins
/**
* Настройки доступа к Bitbucket
*/
class BitbucketSettings {
private final URL url
private final String login
private final String password
BitbucketSettings(URL url, String login, String password) {
this.url = url
this.login = login
this.password = password
}
/**
* URL для доступа, пример - https://bitbucket.example.net
*/
URL getUrl() {
return url
}
/**
* Логин пользователя
*/
String getLogin() {
return login
}
/**
* Пароль пользователя
*/
String getPassword() {
return password
}
}
import JobParams
import JobFactory
@GrabResolver(name = 'central', root = 'http://nexus.example.net/content/repositories/central/')
@Grab('org.codehaus.groovy.modules.http-builder:http-builder:0.7.1')
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.HttpResponseException
/**
* Класс с логикой синхронизации описания сборок в Bitbucket и сборками в Jenkins
* Метаданные это файлы в git репозитории:
* - с содержимым в формате Jenkins job DSL
* - должны начинаться с jenkins_ и иметь расширение groovy
* - должны располагаться в папке jenkins, которая должна лежать в корне репозитория
*
* Логика:
* 1) Динамически обойти все репозитории в указанном проекте в Bitbucket
* 2) Для каждого репозитрия получить весь список бранчей и тегов
* 3) В каждом бранче и теге найти метаописания сборок - скрипты генерации сборок
* 4) Динамически запустить все скрипты сборок, тем самым создав или обновив сборки в Jenkins
*/
class BitbucketSynchronizer {
private static final int MAX_BRANCHES_IN_REPO = 200
private static final int MAX_REPOS_IN_PROJECT = 200
private static final int MAX_LINES_IN_JOB_SCRIPT = 1000
/**
* Внутренний оббъект Jenkins для генерации job
*/
private dslFactory
/**
* Http клиент для вызова Bitbucket по http API
*/
private HTTPBuilder httpBuilder
/**
* Настройки для работы с Bitbucket http API
*/
private BitbucketSettings bitbucketSettings
/**
* Класс с функционалом генерации типовых сборок
*/
private JobFactory jobFactory
/**
* Конструктор
* @param dslFactory служебный объект Jenkins для генерации job
* @param bitbucketSettings настройки подключени к Bitbucket
*/
BitbucketSynchronizer(dslFactory, BitbucketSettings bitbucketSettings) {
this.dslFactory = dslFactory
this.bitbucketSettings = bitbucketSettings
this.jobFactory = new JobFactory(dslFactory)
this.httpBuilder = createHttpBuilder(bitbucketSettings)
}
private HTTPBuilder createHttpBuilder(BitbucketSettings bitbucketSettings) {
HTTPBuilder builder = new HTTPBuilder(bitbucketSettings.url)
builder.ignoreSSLIssues()
String basicAuth = "$bitbucketSettings.login:$bitbucketSettings.password".toString().bytes.encodeBase64()
builder.headers += ["Content-Type" : "application/json",
"Authorization": "Basic $basicAuth"]
return builder
}
/**
* Синхронизация метаданных сборок по данным из проекта в Bitbucket
* @param projectKey ключ прооекта в Bitbucket
* @param jenkinsSynchRoot папка в Jenkins, относительно которой будут генериться сборки
*/
void synchProjectFromBitbucket(String projectKey, String jenkinsSynchRoot) {
def reposUrl = "/rest/api/1.0/projects/$projectKey/repos"
dslFactory.out.println "Getting repos for project: project=$projectKey, URL=$bitbucketSettings.url$reposUrl"
httpBuilder.get(path: reposUrl, query: ["limit": MAX_REPOS_IN_PROJECT]).values.each {
synchRepository(it, jenkinsSynchRoot)
dslFactory.queue(getRepoSynchJobName(jenkinsSynchRoot, it.slug))
}
}
/**
* Синхронизация сборок по репозиторию Bitbucket в указанную папку Jenkins
* @param repository данные репозитория Bitbucket из http API
* @param jenkinsSynchRoot папка в Jenkins, относительно которой будут генериться сборки
*/
private void synchRepository(repository, String jenkinsSynchRoot) {
dslFactory.out.println "Synch repo: $repository.slug"
upsertSynchRepoJob(repository, jenkinsSynchRoot)
def repoUrl = "/rest/api/1.0/projects/$repository.project.key/repos/$repository.slug"
httpBuilder.get(path: "$repoUrl/branches", query: ["limit": MAX_BRANCHES_IN_REPO]).values.each {
String branch = it.id - "refs/heads/"
dslFactory.out.println "Synching branch: $branch"
def jenkinsDirUrl = "$repoUrl/browse/jenkins"
def jenkinsDirContent
try {
jenkinsDirContent = httpBuilder.get(path: jenkinsDirUrl, query: ["at": branch])
} catch (HttpResponseException ignored) {
return
}
jenkinsDirContent.children.values.each {
if (it.path.extension != "groovy" || !it.path.name.startsWith("jenkins_")) {
dslFactory.out.println "$it.path.name will be skipped: should starts with with jenkins_ " +
"and have groovy extension."
return
}
String scriptFileUrl = "$jenkinsDirUrl/$it.path.name"
def jobScriptContent = httpBuilder.get(path: scriptFileUrl, query: ["at": branch,
"limit": MAX_LINES_IN_JOB_SCRIPT])
String fileContent = jobScriptContent.lines.text.join('\n')
String branchForBuild = branch.replaceAll("/", "_")
String jenkinsDirectory = "$jenkinsSynchRoot/$repository.slug/$branchForBuild"
dslFactory.jobParams = JobParams.builder()
.withJenkinsDirectory(jenkinsDirectory)
.withGitRepoUrl(getCloneSshLink(repository))
.withGitBranch(branch)
.build()
dslFactory.jobFactory = jobFactory
jobFactory.jobParams = dslFactory.jobParams
dslFactory.out.println "Executing script: $bitbucketSettings.url$scriptFileUrl?at=$branch"
Eval.x(dslFactory, "x.with{$fileContent}")
}
}
}
void synchRepoFromBitbucket(String projectKey, String repositorySlug, String jenkinsSynchRoot) {
synchRepository(httpBuilder.get(path: "/rest/api/1.0/projects/$projectKey/repos/$repositorySlug"), jenkinsSynchRoot)
}
/**
* Создание сборки для синхронной синхронизации метаданных сборок из репозитория Bitbucket и Jenkins
* @param repo данные репозитория Bitbucket
* @param jenkinsSynchRoot папка в Jenkins, относительно которой будут генериться сборки
*/
private void upsertSynchRepoJob(repo, String jenkinsSynchRoot) {
dslFactory.folder("$jenkinsSynchRoot/$repo.slug")
dslFactory.job(getRepoSynchJobName(jenkinsSynchRoot, repo.slug)) {
configure {
it / 'buildWrappers' / 'EnvInjectPasswordWrapper' << {
injectGlobalPasswords('false')
maskPasswordParameters('true')
passwordEntries() << {
'EnvInjectPasswordEntry' {
name('BITBUCKET_USER')
value(bitbucketSettings.login)
}
'EnvInjectPasswordEntry' {
name('BITBUCKET_PASSWORD')
value(bitbucketSettings.password)
}
}
}
}
environmentVariables {
env('PROJECT_KEY', repo.project.key)
env('REPOSITORY_SLUG', repo.slug)
env('JENKINS_SYNCH_ROOT', jenkinsSynchRoot)
}
multiscm {
git {
remote {
url(getCloneSshLink(repo))
refspec('refs/remotes/origin/*')
}
extensions {
relativeTargetDirectory('repo')
}
}
git {
remote {
url("ssh://git@bitbucket.example.net/jenkins-scripts.git")
branches("refs/heads/master")
}
}
}
steps {
dsl {
external("synchronizeRepo.groovy")
removeAction('DELETE')
}
}
}
}
/**
* Создание сборки для синхронизации метаданных сборок проекта Bitbucket и Jenkins
* @param projectKey ключ проекта Bitbucket
* @param jenkinsSynchRoot папка в Jenkins, относительно которой будут генериться сборки
*/
def upsertSynchProjectJob(String projectKey, String jenkinsSynchRoot) {
dslFactory.folder(jenkinsSynchRoot)
dslFactory.job("$jenkinsSynchRoot/~synch") {
configure {
it / 'buildWrappers' / 'EnvInjectPasswordWrapper' << {
injectGlobalPasswords('false')
maskPasswordParameters('true')
passwordEntries() << {
'EnvInjectPasswordEntry' {
name('BITBUCKET_USER')
value(bitbucketSettings.login)
}
'EnvInjectPasswordEntry' {
name('BITBUCKET_PASSWORD')
value(bitbucketSettings.password)
}
}
}
}
environmentVariables {
env('PROJECT_KEY', projectKey)
env('JENKINS_SYNCH_ROOT', jenkinsSynchRoot)
}
triggers {
cron('H 3 * * *')
}
scm {
git {
remote {
url("ssh://git@bitbucket.example.net/jenkins-scripts.git")
branches("refs/heads/master")
}
}
}
steps {
dsl {
external("synchronizeProject.groovy")
removeAction('DELETE')
}
}
}
}
private static String getRepoSynchJobName(String jenkinsSynchRoot, String repoSlug) {
"$jenkinsSynchRoot/$repoSlug/~synchRepo"
}
private static String getCloneSshLink(repo) {
for (link in repo.links.clone) {
if (link.name == "ssh") {
return link.href
}
}
throw new IllegalArgumentException("No ssh link in: repo=$repo.slug, links=$repo.links")
}
}
jobFactory.releaseJobs()
/**
* Библиотека типовых сборок
* Инстанс этого объекта доступен конечным скриптам генерации сборки
*/
class JobFactory {
private dslFactory
JobParams jobParams
final ReleaseJobFactory releaseJobFactory
/**
* Конструктор
* @param dslFactory служебный объект Jenkins для генерации сборок
*/
JobFactory(dslFactory) {
this.dslFactory = dslFactory
}
/**
* Генерация сборок Jenkins для автоматизации релизов
*/
void releaseJobs() {
if (jobParams.gitBranch == "dev") {
releaseJobFactory.createReleaseBranchJob(jobParams)
}
}
}
/**
* Параметры сборки
* Инстанс этого объекта доступен конечным скриптам генерации сборки
*/
class JobParams {
private final String jenkinsDirectory
private final String gitRepoUrl
private final String gitBranch
JobParams(String jenkinsDirectory, String gitRepoUrl, String gitBranch) {
this.jenkinsDirectory = jenkinsDirectory
this.gitRepoUrl = gitRepoUrl
this.gitBranch = gitBranch
}
/**
* Папка в Jenkins, в которой надо сформировать сборку
* Формируется по принципу <jenkins_synch_root>/<repo_name>/<branch_name>
*/
String getJenkinsDirectory() {
return jenkinsDirectory
}
/**
* URL Git репозитория, для которого формируется сборка
*/
String getGitRepoUrl() {
return gitRepoUrl
}
/**
* Ветка, для которой выполняется скрипт генерации сборки
*/
String getGitBranch() {
return gitBranch
}
static builder() {
return new Builder()
}
static class Builder {
private String jenkinsDirectory
private String gitRepoUrl
private String gitBranch
Builder withJenkinsDirectory(String jenkinsDirectory) {
this.jenkinsDirectory = jenkinsDirectory
return this
}
Builder withGitRepoUrl(String gitRepoUrl) {
this.gitRepoUrl = gitRepoUrl
return this
}
Builder withGitBranch(String gitBranch) {
this.gitBranch = gitBranch
return this
}
JobParams build() {
return new JobParams(jenkinsDirectory, gitRepoUrl, gitBranch)
}
}
}
/**
* Фабрика генерация сборок для автоматизации релизного цикла
*/
class ReleaseJobFactory {
private dslFactory
/**
* Конструктор
* @param dslFactory служебный объект Jenkins для генерации сборок
*/
ReleaseJobFactory(dslFactory) {
this.dslFactory = dslFactory
}
/**
* Срезание релизной ветки
* @param jobParams параметры сборки
*/
void createReleaseBranchJob(JobParams jobParams) {
dslFactory.folder(jobParams.jenkinsDirectory)
dslFactory.job(jobParams.jenkinsDirectory + "/createReleaseBranch") {
jdk('jdk1.8.0')
scm {
git {
remote {
url(jobParams.gitRepoUrl)
refspec('refs/remotes/origin/*')
branches("**/$jobParams.gitBranch")
}
}
}
steps {
gradle {
makeExecutable(true)
description('Срезание релизного бранча')
tasks(':createReleaseBranch')
}
}
}
}
}
/**
* Скрипт создания корневых сборок, который будут синхронизировать Jenkins и Bitbucket на уровне проектов
*/
import BitbucketSynchronizer
import BitbucketSettings
def bitbucketSettings = new BitbucketSettings(new URL("https://bitbucket.example.net"),
BITBUCKET_USER, BITBUCKET_PASSWORD)
BitbucketSynchronizer synchronizer = new BitbucketSynchronizer(this, bitbucketSettings)
synchronizer.upsertSynchProjectJob("BACKEND-SERVICES", "BACKEND/services")
/**
* Скрипт синхронизации Jenkins и Bitbucket на уровне проекта
*/
import BitbucketSynchronizer
import BitbucketSettings
def bitbucketSettings = new BitbucketSettings(new URL("https://bitbucket.example.net"),
BITBUCKET_USER, BITBUCKET_PASSWORD)
new BitbucketSynchronizer(this, bitbucketSettings).synchProjectFromBitbucket(PROJECT_KEY, JENKINS_SYNCH_ROOT)
/**
* Скрипт синхронизации Jenkins и Bitbucket на уровне репозитория
*/
import BitbucketSynchronizer
import BitbucketSettings
def bitbucketSettings = new BitbucketSettings(
new URL("https://bitbucket.example.net"),
BITBUCKET_USER, BITBUCKET_PASSWORD)
new BitbucketSynchronizer(this, bitbucketSettings).synchRepoFromBitbucket(
PROJECT_KEY, REPOSITORY_SLUG, JENKINS_SYNCH_ROOT)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment