Initial commit
Add infrastructure jobs for sandbox
Related-PROD: RE-336
Change-Id: I2140d47e3fc360ab05f92175b29b31e69b2ec10b
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..57b794a
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,28 @@
+apply plugin: 'groovy'
+apply plugin: 'codenarc'
+
+String jcenterRepo = System.getenv('ARTIFACTORY_URL') ?: 'https://artifactory.mcp.mirantis.net/jcenter'
+String rulesFile = 'codenarcRules.groovy'
+
+repositories {
+ maven {
+ url jcenterRepo
+ }
+}
+
+
+sourceSets {
+ main {
+ groovy {
+ exclude rulesFile
+ srcDir '.'
+ }
+ }
+}
+
+compileGroovy.enabled = false
+
+codenarc {
+ configFile = new File(rulesFile)
+ reportFormat = 'console'
+}
diff --git a/codenarcRules.groovy b/codenarcRules.groovy
new file mode 100644
index 0000000..5ce9ca1
--- /dev/null
+++ b/codenarcRules.groovy
@@ -0,0 +1,133 @@
+ruleset {
+ ruleset('rulesets/basic.xml') {
+ EmptyCatchBlock(enabled:false)
+ }
+ ruleset('rulesets/braces.xml')
+ ruleset('rulesets/comments.xml') {
+ // Not necessarily an issue
+ JavadocEmptyFirstLine(enabled:false)
+ JavadocEmptyLastLine(enabled:false)
+ JavadocMissingParamDescription(enabled:false)
+ JavadocConsecutiveEmptyLines(enabled:false)
+ }
+ ruleset('rulesets/concurrency.xml') {
+ // Not necessarily an issue
+ BusyWait(enabled:false)
+ }
+ ruleset('rulesets/convention.xml') {
+ // Don't need due to code readablilty
+ NoDef(enabled:false)
+ // Not necessarily an issue
+ CompileStatic(enabled:false)
+ // TBD
+ CouldBeElvis(enabled:false)
+ // Not necessarily an issue
+ TrailingComma(enabled:false)
+ // Not necessarily an issue
+ VariableTypeRequired(enabled:false)
+ // Not necessarily an issue
+ FieldTypeRequired(enabled:false)
+ // Not necessarily an issue
+ MethodParameterTypeRequired(enabled:false)
+ // Not necessarily an issue
+ MethodReturnTypeRequired(enabled:false)
+ // Not necessarily an issue
+ NoJavaUtilDate(enabled:false)
+ }
+ ruleset('rulesets/design.xml') {
+ // Don't need due to code readablilty
+ BuilderMethodWithSideEffects(enabled:false)
+ // Sometimes nested loop is cleaner than extracting a new method
+ NestedForLoop(enabled:false)
+ // TBD
+ ImplementationAsType(enabled:false)
+ // Not necessarily an issue
+ Instanceof(enabled:false)
+ }
+ ruleset('rulesets/dry.xml') {
+ DuplicateNumberLiteral(enabled: false)
+ DuplicateStringLiteral(enabled: false)
+ DuplicateMapLiteral(enabled: false)
+ DuplicateListLiteral(enabled: false)
+ }
+// Raises a lot of "Compilation failed" warnings
+// ruleset('rulesets/enhanced.xml')
+ ruleset('rulesets/exceptions.xml') {
+ // Not necessarily an issue
+ CatchException(enabled:false)
+ // Not necessarily an issue
+ ThrowRuntimeException(enabled:false)
+ // Not necessarily an issue
+ ReturnNullFromCatchBlock(enabled:false)
+
+ }
+ ruleset('rulesets/formatting.xml') {
+ // Don't need due to code readablilty
+ ConsecutiveBlankLines(enabled:false)
+ // TBD
+ SpaceAfterClosingBrace(enabled:false)
+ SpaceBeforeOpeningBrace(enabled:false)
+ // Enforce at least one space after map entry colon
+ //SpaceAroundMapEntryColon {
+ // characterAfterColonRegex = /\s/
+ // characterBeforeColonRegex = /./
+ //}
+ SpaceAroundMapEntryColon(enabled:false)
+ //TBD
+ Indentation(enabled:false)
+ // TBD
+ LineLength(enabled: false)
+ BlockStartsWithBlankLine(enabled: false)
+ BlockEndsWithBlankLine(enabled: false)
+ }
+ ruleset('rulesets/generic.xml')
+ ruleset('rulesets/grails.xml')
+ ruleset('rulesets/groovyism.xml') {
+ // Not necessarily an issue
+ GStringExpressionWithinString(enabled:false)
+ }
+ ruleset('rulesets/imports.xml')
+ ruleset('rulesets/jdbc.xml')
+ ruleset('rulesets/junit.xml')
+ ruleset('rulesets/logging.xml') {
+ // Can't be used in jenklins pipelines
+ Println(enabled:false)
+ }
+ ruleset('rulesets/naming.xml') {
+ // Don't need due to code readablilty
+ FactoryMethodName(enabled:false)
+ // Don't need due to code readablilty
+ VariableName(enabled:false)
+ }
+ ruleset('rulesets/security.xml') {
+ // Don't need to satisfy the Java Beans specification
+ JavaIoPackageAccess(enabled:false)
+ }
+ ruleset('rulesets/serialization.xml')
+ ruleset('rulesets/size.xml') {
+ // TBD
+ AbcMetric(enabled:false)
+ // TBD
+ MethodSize(enabled:false)
+ // TBD
+ NestedBlockDepth(enabled:false)
+ // Not necessarily an issue
+ ParameterCount(enabled:false)
+ CyclomaticComplexity(enabled:false)
+ }
+ ruleset('rulesets/unnecessary.xml') {
+ // Don't need due to code readablilty
+ UnnecessaryDefInVariableDeclaration(enabled:false)
+ // Not necessarily an issue
+ UnnecessaryGetter(enabled:false)
+ // Not necessarily an issue
+ UnnecessarySetter(enabled:false)
+ // Not necessarily an issue
+ UnnecessaryReturnKeyword(enabled:false)
+ // Not necessarily an issue
+ UnnecessaryObjectReferences(enabled:false)
+ // Not necessarily an issue
+ UnnecessaryCollectCall(enabled:false)
+ }
+ ruleset('rulesets/unused.xml')
+}
diff --git a/common/pipelines/codenarc.groovy b/common/pipelines/codenarc.groovy
new file mode 100644
index 0000000..c4ae040
--- /dev/null
+++ b/common/pipelines/codenarc.groovy
@@ -0,0 +1,94 @@
+#!groovy
+
+def main() {
+ String gitUrl = "${env.GERRIT_SCHEME}://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ String gitRef = env.GERRIT_REFSPEC
+
+ stage('SCM checkout') {
+ echo "Checking out git repository from ${gitUrl} @ ${gitRef}"
+
+ checkout \
+ $class: 'GitSCM',
+ branches: [[
+ name: 'FETCH_HEAD'
+ ]],
+ userRemoteConfigs: [[
+ url: gitUrl,
+ refspec: gitRef,
+ credentialsId: env.GIT_CREDENTIALS_ID
+ ]],
+ extensions: [[
+ $class: 'WipeWorkspace'
+ ]]
+ }
+
+ stage('Codenarc') {
+ String corenarcRulesFile = 'codenarcRules.groovy'
+ if (!fileExists(corenarcRulesFile)) {
+ writeFile \
+ file: corenarcRulesFile,
+ text: env.DEFAULT_RULES
+ }
+ sh '''#!/bin/bash -ex
+ codenarc \
+ -maxPriority1Violations=0 \
+ -maxPriority2Violations=0 \
+ -maxPriority3Violations=0 \
+ -excludes='**/codenarcRules.groovy' \
+ -rulesetfiles=file:codenarcRules.groovy \
+ -report=console \
+ -report=html:report.html \
+ | tee report.log
+ if [ "${PIPESTATUS[0]}" != '0' ]; then
+ exit 1
+ fi
+ if grep -q 'Compilation failed' report.log ; then
+ exit 1
+ fi
+ if grep -q 'Error processing' report.log ; then
+ exit 1
+ fi
+ '''
+ }
+
+ stage('Report') {
+ archiveArtifacts \
+ artifacts: 'report.html',
+ allowEmptyArchive: true
+ }
+}
+
+String podTpl = """
+ apiVersion: "v1"
+ kind: "Pod"
+ spec:
+ securityContext:
+ runAsUser: 1000
+ containers:
+ - name: "codenarc"
+ image: "${env.DOCKER_IMAGE}"
+ command:
+ - "cat"
+ securityContext:
+ privileged: false
+ tty: true
+"""
+if (env.K8S_CLUSTER == 'unset') {
+ node(env.NODE_LABEL) {
+ docker.image(env.DOCKER_IMAGE).inside('--entrypoint=""') {
+ main()
+ }
+ }
+} else {
+ podTemplate(
+ cloud: env.K8S_CLUSTER,
+ yaml: podTpl,
+ showRawYaml: false
+ ) {
+ node(POD_LABEL) {
+ container('codenarc') {
+ main()
+ }
+ }
+ }
+}
diff --git a/common/pipelines/shellcheck.groovy b/common/pipelines/shellcheck.groovy
new file mode 100644
index 0000000..66a7d37
--- /dev/null
+++ b/common/pipelines/shellcheck.groovy
@@ -0,0 +1,65 @@
+#!groovy
+
+def main() {
+ String gitUrl = "${env.GERRIT_SCHEME}://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ String gitRef = env.GERRIT_REFSPEC
+
+ stage('SCM checkout') {
+ checkout \
+ $class: 'GitSCM',
+ branches: [[
+ name: 'FETCH_HEAD'
+ ]],
+ userRemoteConfigs: [[
+ url: gitUrl,
+ refspec: gitRef,
+ credentialsId: env.GIT_CREDENTIALS_ID
+ ]],
+ extensions: [[
+ $class: 'WipeWorkspace'
+ ]]
+ }
+
+ stage('Shellcheck') {
+ sh '''#!/bin/bash -ex
+ git show --name-only --diff-filter=AM \
+ | grep -E '.sh$' \
+ | xargs --no-run-if-empty shellcheck -e SC1090,SC2013,SC2154,SC2029
+ '''
+ }
+}
+
+String podTpl = """
+ apiVersion: "v1"
+ kind: "Pod"
+ spec:
+ securityContext:
+ runAsUser: 1000
+ containers:
+ - name: "main"
+ image: "${env.DOCKER_IMAGE}"
+ command:
+ - "cat"
+ securityContext:
+ privileged: false
+ tty: true
+"""
+if (env.K8S_CLUSTER == 'unset') {
+ node(env.NODE_LABEL) {
+ docker.image(env.DOCKER_IMAGE).inside('--entrypoint=""') {
+ main()
+ }
+ }
+} else {
+ podTemplate(
+ cloud: env.K8S_CLUSTER,
+ yaml: podTpl,
+ showRawYaml: false
+ ) {
+ node(POD_LABEL) {
+ container('main') {
+ main()
+ }
+ }
+ }
+}
diff --git a/common/pipelines/test-jenkins-jobs.groovy b/common/pipelines/test-jenkins-jobs.groovy
new file mode 100644
index 0000000..e04f644
--- /dev/null
+++ b/common/pipelines/test-jenkins-jobs.groovy
@@ -0,0 +1,218 @@
+#!groovy
+//import groovy.transform.Field
+
+String getJobs(getJobsCmd) {
+ String result
+ dir("${env.WORKSPACE}/output/${env.CI_NAME}") {
+ result = sh \
+ script: """\
+ ${getJobsCmd} \
+ | grep -v '\\/\$' \
+ | grep -E '^(deleting|Files)' \
+ | sed -r 's%^(deleting|Files)\\s%%g' \
+ | sed -r 's%^old/%%' \
+ | cut -d' ' -f1
+ """,
+ returnStdout: true
+ }
+ return result
+}
+
+def main() {
+ // Use gerrit parameters if set with fallback to job param
+ String gitUrl = "${env.GERRIT_SCHEME}://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ String gitRef = env.GERRIT_REFSPEC
+
+ String pathSep = '/'
+
+ String getAddedJobsCmd = 'rsync --dry-run -av --delete old/ new/'
+ String getRemovedJobsCmd = 'rsync --dry-run -av --delete new/ old/'
+ String getDiffJobsCmd = 'diff -rq old/ new/'
+
+ // Set current build description
+ if (env.GERRIT_CHANGE_URL) {
+ currentBuild.description = """
+ <p>
+ Triggered by change: <a href="${env.GERRIT_CHANGE_URL}">${env.GERRIT_CHANGE_NUMBER},${env.GERRIT_PATCHSET_NUMBER}</a><br/>
+ Project: <b>${env.GERRIT_PROJECT}</b><br/>
+ Branch: <b>${env.GERRIT_BRANCH}</b><br/>
+ Subject: <b>${env.GERRIT_CHANGE_SUBJECT}</b><br/>
+ </p>
+ """
+ }
+
+ // Get & prepare source code
+ stage('SCM checkout') {
+ echo "Checking out git repository from ${gitUrl} @ ${gitRef}"
+
+ checkout \
+ $class: 'GitSCM',
+ branches: [
+ [name: 'FETCH_HEAD'],
+ ],
+ userRemoteConfigs: [
+ [url: gitUrl, refspec: gitRef, credentialsId: env.GIT_CREDENTIALS_ID],
+ ],
+ extensions: [
+ [$class: 'WipeWorkspace'],
+ ]
+ }
+
+ stage('Check for non-ascii characters') {
+ def asciiStatus = sh \
+ script: 'grep -q --perl-regexp -R "[^[:ascii:]]" --include \\*.sh --include \\*.yaml --include \\*.groovy *',
+ returnStatus: true
+
+ if (asciiStatus == 0) {
+ error 'Found non-ASCII symbols!!!'
+ }
+ }
+
+ stage('JJB verify') {
+ withEnv(['HOME=/tmp/']) {
+ // Generate current jobs from parent commit (will be used for diff)
+ sh 'tox -v -e compare-xml-new'
+ }
+ }
+
+ stage('JJB compare') {
+ withEnv(['HOME=/tmp/']) {
+ // Generate jobs from parent commit (will be used for diff)
+ sh '''
+ git reset --hard HEAD^
+ git checkout FETCH_HEAD -- tox.ini
+ tox -v -e compare-xml-old
+ '''
+ }
+
+ dir("output/${env.CI_NAME}") {
+ Integer diffStatus = sh \
+ script: 'diff -rq old/ new/',
+ returnStatus: true
+
+ if (diffStatus == 0) {
+ currentBuild.result = 'SUCCESS'
+ currentBuild.description += 'No job changes'
+ currentBuild.getRawBuild().getExecutor().interrupt(Result.SUCCESS)
+ sleep(1) // Interrupt is not blocking and does not take effect immediately.
+ }
+ }
+
+ // Analyse output file and prepare array with results
+
+ String diffJobs = getJobs(getDiffJobsCmd)
+ String addedJobs = getJobs(getAddedJobsCmd)
+ String removedJobs = getJobs(getRemovedJobsCmd)
+
+ // Set job description
+
+ String description = ''
+ String _item, _itemPath
+
+ dir("output/${env.CI_NAME}") {
+ if (diffJobs.size() > 0) {
+ description += '<b>CHANGED</b><ul>'
+ diffJobs.split('\n').each { item ->
+ _item = item.replace('/config.xml', '')
+ try {
+ _itemPath = item.tokenize(pathSep)[0..-2].join(pathSep)
+ } catch (e) {
+ _itemPath = ''
+ }
+ description += "<li><a href=\"${env.BUILD_URL}artifact/output/${env.CI_NAME}/diff/${item}/*view*/\">${_item}</a></li>"
+
+ // Generate diff file
+ sh """
+ mkdir -p diff/${_itemPath}
+ diff -U 50 \
+ 'old/${item}' \
+ 'new/${item}' \
+ > 'diff/${item}' || :
+ """
+ }
+ description += '</ul>'
+ }
+
+ if (addedJobs.size() > 0) {
+ description += '<b>ADDED</b><ul>'
+ addedJobs.split('\n').each { item ->
+ _item = item.replace('/config.xml', '')
+ try {
+ _itemPath = item.tokenize(pathSep)[0..-2].join(pathSep)
+ } catch (e) {
+ _itemPath = ''
+ }
+ description += "<li><a href=\"${env.BUILD_URL}artifact/output/${env.CI_NAME}/diff/${item}/*view*/\">${_item}</a></li>"
+ sh """
+ mkdir -p diff/${_itemPath}
+ cp new/${item} diff/${_itemPath}/
+ """
+ }
+ description += '</ul>'
+ }
+
+ if (removedJobs.size() > 0) {
+ description += '<b>DELETED</b><ul>'
+ removedJobs.split('\n').each { item ->
+ _item = item.replace('/config.xml', '')
+ try {
+ _itemPath = item.tokenize(pathSep)[0..-2].join(pathSep)
+ } catch (e) {
+ _itemPath = ''
+ }
+ description += "<li><a href=\"${env.BUILD_URL}artifact/output/${env.CI_NAME}/diff/${item}/*view*/\">${_item}</a></li>"
+ sh """
+ mkdir -p diff/${_itemPath}
+ cp old/${item} diff/${_itemPath}/
+ """
+ }
+ description += '</ul>'
+ }
+ }
+
+ currentBuild.description += description
+ }
+
+ // Save results
+ stage('Record test results') {
+ archiveArtifacts([
+ artifacts: "output/${env.CI_NAME}/diff/**",
+ allowEmptyArchive: true,
+ ])
+ }
+}
+
+String podTpl = """
+ apiVersion: "v1"
+ kind: "Pod"
+ spec:
+ securityContext:
+ runAsUser: 1000
+ containers:
+ - name: "tox"
+ image: "${env.DOCKER_IMAGE}"
+ command:
+ - "cat"
+ securityContext:
+ privileged: false
+ tty: true
+"""
+if (env.K8S_CLUSTER == 'unset') {
+ node(env.SLAVE_LABEL ?: 'docker') {
+ docker.image(env.DOCKER_IMAGE).inside('--entrypoint=""') {
+ main()
+ }
+ }
+} else {
+ podTemplate(
+ cloud: env.K8S_CLUSTER,
+ yaml: podTpl,
+ showRawYaml: false
+ ) {
+ node(POD_LABEL) {
+ container('tox') {
+ main()
+ }
+ }
+ }
+}
diff --git a/common/pipelines/tox.groovy b/common/pipelines/tox.groovy
new file mode 100644
index 0000000..f0d8ccf
--- /dev/null
+++ b/common/pipelines/tox.groovy
@@ -0,0 +1,67 @@
+#!groovy
+
+def main() {
+ String gitUrl = "${env.GERRIT_SCHEME}://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ String gitRef = env.GERRIT_REFSPEC
+
+ stage('SCM checkout') {
+ echo "Checking out git repository from ${gitUrl} @ ${gitRef}"
+
+ checkout \
+ $class: 'GitSCM',
+ branches: [[
+ name: 'FETCH_HEAD'
+ ]],
+ userRemoteConfigs: [[
+ url: gitUrl,
+ refspec: gitRef,
+ credentialsId: env.GIT_CREDENTIALS_ID
+ ]],
+ extensions: [[
+ $class: 'WipeWorkspace'
+ ]]
+ }
+
+ stage('tox') {
+ sh '''#!/bin/bash -ex
+ tox -v
+ '''
+
+ }
+}
+
+String podTpl = """
+ apiVersion: "v1"
+ kind: "Pod"
+ spec:
+ securityContext:
+ runAsUser: 1000
+ containers:
+ - name: "main"
+ image: "${env.DOCKER_IMAGE}"
+ command:
+ - "cat"
+ securityContext:
+ privileged: false
+ tty: true
+"""
+
+if (env.K8S_CLUSTER == 'unset') {
+ node(env.NODE_LABEL) {
+ docker.image(env.DOCKER_IMAGE).inside('--entrypoint=""') {
+ main()
+ }
+ }
+} else {
+ podTemplate(
+ cloud: env.K8S_CLUSTER,
+ yaml: podTpl,
+ showRawYaml: false
+ ) {
+ node(POD_LABEL) {
+ container('main') {
+ main()
+ }
+ }
+ }
+}
diff --git a/common/pipelines/update-jenkins-config.groovy b/common/pipelines/update-jenkins-config.groovy
new file mode 100644
index 0000000..0e76192
--- /dev/null
+++ b/common/pipelines/update-jenkins-config.groovy
@@ -0,0 +1,75 @@
+#!groovy
+import io.jenkins.plugins.casc.ConfigurationAsCode
+
+@NonCPS
+Boolean testConfig(String cascPath) {
+ def casc = ConfigurationAsCode.get()
+ def form
+ Boolean result = true
+ try {
+ form = casc.doCheckNewSource(cascPath)
+ } catch (Exception e) {
+ result = false
+ message = e.toString()
+ } finally {
+ if (form) {
+ println form.kind
+ println form.message.split('<')[0]
+ if (form.kind.toString() != 'OK') {
+ result = false
+ }
+ }
+ }
+ return result
+}
+
+@NonCPS
+void applyConfig(String cascPath) {
+ ConfigurationAsCode.get().configure(cascPath)
+}
+
+node('master') {
+ Boolean doApply = false
+ String cascPath = env.WORKSPACE
+
+ if (env.GERRIT_EVENT_TYPE == 'ref-updated') {
+ doApply = true
+ cascPath = ConfigurationAsCode.get().getStandardConfig()[0] ?: "${env.HOME}/casc"
+ }
+
+ stage('SCM checkout') {
+ String refSpec = env.GERRIT_REFSPEC ?: env.GERRIT_REFNAME
+ String gitUrl = "ssh://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ echo "Checking out git repository from ${env.GERRIT_PROJECT} @ ${refSpec}"
+
+ dir(cascPath) {
+ checkout([
+ $class: 'GitSCM',
+ branches: [[
+ name: 'FETCH_HEAD',
+ ]],
+ userRemoteConfigs: [[
+ url: gitUrl,
+ refspec: refSpec,
+ credentialsId: env.GIT_CREDENTIALS_ID,
+ ]],
+ extensions: [[
+ $class: 'WipeWorkspace',
+ ]],
+ ])
+ }
+ }
+
+ stage('Test configuration') {
+ if (! testConfig(cascPath) ) {
+ currentBuild.result = 'FAILURE'
+ throw new RuntimeException ('Fail')
+ }
+ }
+
+ if (doApply) {
+ stage('Apply configuration') {
+ applyConfig(cascPath)
+ }
+ }
+}
diff --git a/common/pipelines/update-jenkins-jobs.groovy b/common/pipelines/update-jenkins-jobs.groovy
new file mode 100644
index 0000000..4bb3702
--- /dev/null
+++ b/common/pipelines/update-jenkins-jobs.groovy
@@ -0,0 +1,202 @@
+#!groovy
+
+def main(String cacheHome = 'output/cache') {
+ String gitUrl = "${env.GERRIT_SCHEME}://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ String gitRef = env.GERRIT_BRANCH ?: env.GERRIT_REFNAME
+ String artInfraNamespace = 'binary-dev-local/infra'
+ String artCacheFile = "${env.CI_NAME}.jjb.zip"
+ String artCacheUrl = "${env.ARTIFACTORY_URL}/artifactory/${artInfraNamespace}/${artCacheFile}"
+ String artCredential = env.ART_CREDENTIALS_ID
+ String jenkinsCredential = env.JENKINS_CREDENTIALS_ID
+ def response
+
+ def jenkinsJobs
+ def jjbJobs
+ def jobsToRemove
+
+ currentBuild.description = ''
+
+ // Set current build description
+ if (env.GERRIT_CHANGE_URL) {
+ currentBuild.description = """
+ <p>
+ Triggered by change: <a href="${env.GERRIT_CHANGE_URL}">${env.GERRIT_CHANGE_NUMBER},${env.GERRIT_PATCHSET_NUMBER}</a><br/>
+ Project: <b>${env.GERRIT_PROJECT}</b><br/>
+ Branch: <b>${env.GERRIT_BRANCH}</b><br/>
+ Subject: <b>${env.GERRIT_CHANGE_SUBJECT}</b><br/>
+ </p>
+ """
+ }
+
+ stage('SCM checkout') {
+ if (env.MAINTAIN_MODE.toLowerCase() == 'false') {
+ checkout([
+ $class: 'GitSCM',
+ branches: [[
+ name: 'FETCH_HEAD'
+ ]],
+ userRemoteConfigs: [[
+ url: gitUrl,
+ refspec: gitRef,
+ credentialsId: env.GIT_CREDENTIALS_ID
+ ]],
+ extensions: [[
+ $class: 'WipeWorkspace'
+ ]],
+ ])
+ }
+ }
+
+ stage('Get JJB cache') {
+ dir(cacheHome) {
+ response = httpRequest \
+ url: artCacheUrl,
+ authentication: artCredential,
+ httpMode: 'GET',
+ outputFile: artCacheFile,
+ validResponseCodes: '100:399,404'
+ if (response.status != 404) {
+ unzip \
+ zipFile: artCacheFile,
+ dir: '.cache'
+ }
+ }
+ }
+
+ stage('Update JJB jobs') {
+ if (env.MAINTAIN_MODE.toLowerCase() == 'false') {
+ withCredentials([
+ usernamePassword(
+ credentialsId: env.JENKINS_CREDENTIALS_ID,
+ usernameVariable: 'JJB_USER',
+ passwordVariable: 'JJB_PASSWORD')
+ ]) {
+ withEnv([
+ "HOME=${cacheHome}"
+ ]) {
+ sh 'tox -v -e update $JOBS_LIST'
+ }
+ }
+ } else {
+ input \
+ message: 'Sleeping for maintainance'
+ }
+ }
+
+ stage('Get deployed jobs list') {
+ response = httpRequest \
+ url: "${JENKINS_URL}/crumbIssuer/api/json",
+ authentication: jenkinsCredential
+ def crumb = readJSON text: response.getContent()
+
+ response = httpRequest \
+ url: "${JENKINS_URL}/api/json?tree=jobs[name,description]",
+ authentication: jenkinsCredential,
+ customHeaders: [[
+ name: crumb.crumbRequestField,
+ value: crumb.crumb,
+ maskValue: true
+ ]]
+ def jobs = readJSON text: response.getContent()
+
+ jenkinsJobs = jobs.jobs.findAll {
+ // Filter jenkins jobs deployed by JJB
+ it.description.toString().contains('Managed by Jenkins Job Builder')
+ }.collect{ it.name }
+ }
+
+ stage('Get JJB jobs list') {
+ withEnv([
+ "HOME=${cacheHome}"
+ ]) {
+ sh 'tox -v -e jobs'
+ }
+ dir("output/${env.CI_NAME}") {
+ jjbJobs = sh(
+ script: "grep -rl actions | sed 's|/config.xml\$||g'",
+ returnStdout: true
+ ).trim().readLines()
+ }
+
+ if (jjbJobs.size() == 0) {
+ error 'ERROR: Unexpected JJB output. No generated jobs found'
+ }
+ }
+
+ stage('Remove undefined jobs') {
+ jobsToRemove = jenkinsJobs.findAll { ! jjbJobs.contains(it) }
+
+ if (jobsToRemove.size() > 0) {
+ withCredentials([
+ usernamePassword(
+ credentialsId: jenkinsCredential,
+ usernameVariable: 'JJB_USER',
+ passwordVariable: 'JJB_PASSWORD')
+ ]) {
+ withEnv([
+ "HOME=${cacheHome}"
+ ]) {
+ sh "tox -v -e delete ${jobsToRemove.join(' ')}"
+ }
+ }
+
+ String description = '<b>DELETED</b><ul>'
+ jobsToRemove.each { description += "<li>${it}</li>" }
+ description += '</ul>'
+ currentBuild.description += description
+ } else {
+ currentBuild.description += 'No jobs to remove'
+ }
+ }
+
+ stage('Save JJB cache') {
+ dir(cacheHome) {
+ sh "rm -f ${artCacheFile}"
+ zip \
+ zipFile: artCacheFile,
+ dir: '.cache',
+ glob: 'jenkins_jobs/**'
+ response = httpRequest \
+ url: artCacheUrl,
+ authentication: artCredential,
+ httpMode: 'PUT',
+ multipartName: 'file',
+ uploadFile: artCacheFile
+ }
+ }
+}
+
+String podTpl = """
+ apiVersion: "v1"
+ kind: "Pod"
+ spec:
+ securityContext:
+ runAsUser: 1000
+ containers:
+ - name: "tox"
+ image: "${env.DOCKER_IMAGE}"
+ command:
+ - "cat"
+ securityContext:
+ privileged: false
+ tty: true
+"""
+
+if (env.K8S_CLUSTER == 'unset') {
+ node(env.SLAVE_LABEL ?: 'docker') {
+ main(env.WORKSPACE)
+ }
+} else {
+ podTemplate(
+ cloud: env.K8S_CLUSTER,
+ yaml: podTpl,
+ showRawYaml: false
+ ) {
+ node(POD_LABEL) {
+ container('tox') {
+ main()
+ }
+ }
+ }
+}
+
diff --git a/common/pipelines/yamllint.groovy b/common/pipelines/yamllint.groovy
new file mode 100644
index 0000000..94b618a
--- /dev/null
+++ b/common/pipelines/yamllint.groovy
@@ -0,0 +1,70 @@
+#!groovy
+
+def main() {
+ String gitUrl = "${env.GERRIT_SCHEME}://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ String gitRef = env.GERRIT_REFSPEC
+
+ stage('SCM checkout') {
+ echo "Checking out git repository from ${gitUrl} @ ${gitRef}"
+
+ checkout \
+ $class: 'GitSCM',
+ branches: [[
+ name: 'FETCH_HEAD'
+ ]],
+ userRemoteConfigs: [[
+ url: gitUrl,
+ refspec: gitRef,
+ credentialsId: env.GIT_CREDENTIALS_ID
+ ]],
+ extensions: [[
+ $class: 'WipeWorkspace'
+ ]]
+ }
+
+ stage('Yamllint') {
+ sh '''#!/bin/bash -ex
+ YAMLLINT=$(which yamllint)
+ [ -f '.yamllint' ] && YAMLLINT="${YAMLLINT} -c .yamllint"
+ git diff HEAD^ --name-only --diff-filter=AM \
+ | grep -E '\\.ya?ml$' \
+ | xargs --no-run-if-empty ${YAMLLINT}
+ '''
+
+ }
+}
+
+String podTpl = """
+ apiVersion: "v1"
+ kind: "Pod"
+ spec:
+ securityContext:
+ runAsUser: 1000
+ containers:
+ - name: "yamllint"
+ image: "${env.DOCKER_IMAGE}"
+ command:
+ - "cat"
+ securityContext:
+ privileged: false
+ tty: true
+"""
+if (env.K8S_CLUSTER == 'unset') {
+ node(env.NODE_LABEL) {
+ docker.image(env.DOCKER_IMAGE).inside('--entrypoint=""') {
+ main()
+ }
+ }
+} else {
+ podTemplate(
+ cloud: env.K8S_CLUSTER,
+ yaml: podTpl,
+ showRawYaml: false
+ ) {
+ node(POD_LABEL) {
+ container('yamllint') {
+ main()
+ }
+ }
+ }
+}
diff --git a/common/pvc-for-jjb-update.yaml.k8s b/common/pvc-for-jjb-update.yaml.k8s
new file mode 100644
index 0000000..aebe156
--- /dev/null
+++ b/common/pvc-for-jjb-update.yaml.k8s
@@ -0,0 +1,11 @@
+---
+apiVersion: "v1"
+kind: "PersistentVolumeClaim"
+metadata:
+ name: "jjb-cache"
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 2Gi
diff --git a/common/templates/codenarc.yaml b/common/templates/codenarc.yaml
new file mode 100644
index 0000000..4843314
--- /dev/null
+++ b/common/templates/codenarc.yaml
@@ -0,0 +1,37 @@
+---
+
+- job-template:
+ name: infra.codenarc
+ id: common/codenarc
+ project-type: pipeline
+ description: Check groovy scripts by CodeNarc
+ concurrent: True
+
+ properties:
+ - build-discarder:
+ days-to-keep: 14
+ - inject:
+ properties-content: |
+ DOCKER_IMAGE=docker-prod-local.docker.mirantis.net/infra/codenarc:latest
+ K8S_CLUSTER={k8s_cluster}
+ NODE_LABEL={codenarc_node|docker}
+ GIT_CREDENTIALS_ID={git-credentials-id}
+
+
+ parameters:
+ - hidden:
+ name: DEFAULT_RULES
+ default: !include-raw-escape: ../../codenarcRules.groovy
+
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ projects: '{obj:projects}'
+ trigger-on:
+ - patchset-created-event:
+ exclude-drafts: True
+ custom-url: '- ${{JOB_NAME}} ${{BUILD_URL}}artifact/report.html'
+ skip-vote:
+ notbuilt: true
+
+ dsl: !include-raw-escape: ../pipelines/codenarc.groovy
diff --git a/common/templates/shellcheck.yaml b/common/templates/shellcheck.yaml
new file mode 100644
index 0000000..d989d04
--- /dev/null
+++ b/common/templates/shellcheck.yaml
@@ -0,0 +1,31 @@
+---
+- job-template:
+ name: infra.shellcheck
+ id: common/shellcheck
+ project-type: pipeline
+ description: Check shell scripts by ShellCheck
+ concurrent: True
+
+ properties:
+ - build-discarder:
+ days-to-keep: 14
+ - inject:
+ properties-content: |
+ K8S_CLUSTER={k8s_cluster}
+ DOCKER_IMAGE={docker-dev-virtual}/mirantis/openstack-ci/jenkins-job-tests:latest
+ NODE_LABEL={shellcheck_node|docker}
+ GIT_CREDENTIALS_ID={git-credentials-id}
+
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ projects: '{obj:projects}'
+ trigger-on:
+ - patchset-created-event:
+ exclude-drafts: True
+ custom-url: '- ${{JOB_NAME}} ${{BUILD_URL}}console'
+ skip-vote:
+ notbuilt: true
+
+ dsl: !include-raw-escape: ../pipelines/shellcheck.groovy
+
diff --git a/common/templates/tox.yaml b/common/templates/tox.yaml
new file mode 100644
index 0000000..e837ec9
--- /dev/null
+++ b/common/templates/tox.yaml
@@ -0,0 +1,29 @@
+---
+- job-template:
+ name: infra.tox
+ id: common/tox
+ project-type: pipeline
+ description: Run tox tests
+ concurrent: True
+
+ properties:
+ - build-discarder:
+ days-to-keep: 14
+ - inject:
+ properties-content: |
+ K8S_CLUSTER={k8s_cluster}
+ DOCKER_IMAGE={docker-dev-virtual}/mirantis/openstack-ci/jenkins-job-tests:latest
+ NODE_LABEL={tox_node|docker}
+ GIT_CREDENTIALS_ID={git-credentials-id}
+
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ projects: '{obj:projects}'
+ trigger-on:
+ - patchset-created-event:
+ exclude-drafts: True
+ custom-url: '- ${{JOB_NAME}} ${{BUILD_URL}}console'
+ skip-vote: '{obj:skip_vote}'
+
+ dsl: !include-raw-escape: ../pipelines/tox.groovy
diff --git a/common/templates/yamllint.yaml b/common/templates/yamllint.yaml
new file mode 100644
index 0000000..eb5b341
--- /dev/null
+++ b/common/templates/yamllint.yaml
@@ -0,0 +1,29 @@
+---
+- job-template:
+ name: infra.yamllint
+ id: common/yamllint
+ project-type: pipeline
+ description: Check yaml files by yamllint.
+ concurrent: True
+
+ properties:
+ - build-discarder:
+ days-to-keep: 14
+ - inject:
+ properties-content: |
+ K8S_CLUSTER={k8s_cluster}
+ DOCKER_IMAGE={docker-dev-virtual}/mirantis/openstack-ci/jenkins-job-tests:latest
+ NODE_LABEL={yamllint_node|docker}
+ GIT_CREDENTIALS_ID={git-credentials-id}
+
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ projects: '{obj:projects}'
+ trigger-on:
+ - patchset-created-event:
+ exclude-drafts: True
+ custom-url: '- ${{JOB_NAME}} ${{BUILD_URL}}console'
+ skip-vote: '{obj:skip_vote}'
+
+ dsl: !include-raw-escape: ../pipelines/yamllint.groovy
diff --git a/common/test-jenkins-jobs.yaml b/common/test-jenkins-jobs.yaml
new file mode 100644
index 0000000..80f71e8
--- /dev/null
+++ b/common/test-jenkins-jobs.yaml
@@ -0,0 +1,46 @@
+---
+- project:
+ name: test-jenkins-jobs
+ jobs:
+ - infra/jenkins-jobs.check
+
+- job-template:
+ name: infra.jenkins-jobs.check
+ id: infra/jenkins-jobs.check
+ project-type: pipeline
+ description: Check job definitions by Jenkins Job Builder
+ concurrent: True
+
+ properties:
+ - build-discarder:
+ days-to-keep: 14
+ - inject:
+ properties-content: |
+ K8S_CLUSTER={k8s_cluster}
+ GIT_CREDENTIALS_ID={git-credentials-id}
+ DOCKER_IMAGE={docker-dev-virtual}/mirantis/openstack-ci/jenkins-job-tests:latest
+ CI_NAME={ci_name}
+
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ projects:
+ - project-compare-type: PLAIN
+ project-pattern: mcp-ci/jenkins-jobs
+ branches:
+ - branch-pattern: 'master'
+ file-paths:
+ - compare-type: ANT
+ pattern: 'common/*'
+ - compare-type: ANT
+ pattern: 'common/**/*'
+ - compare-type: ANT
+ pattern: 'servers/{ci_name}/*'
+ - compare-type: ANT
+ pattern: 'servers/{ci_name}/**/*'
+ trigger-on:
+ - patchset-created-event:
+ exclude-drafts: True
+ custom-url: '- ${{JOB_NAME}} ${{BUILD_URL}}'
+
+ dsl: !include-raw-escape: pipelines/test-jenkins-jobs.groovy
diff --git a/common/update-jenkins-config.yaml b/common/update-jenkins-config.yaml
new file mode 100644
index 0000000..9be1f85
--- /dev/null
+++ b/common/update-jenkins-config.yaml
@@ -0,0 +1,36 @@
+- project:
+ name: jenkins-config
+ jobs:
+ - infra/jenkins-config.checkupdate
+
+- job-template:
+ name: 'infra.jenkins-config.checkupdate'
+ id: infra/jenkins-config.checkupdate
+ description: |
+ <p>Verify and apply JCasC config</p>
+ project-type: pipeline
+ properties:
+ - build-discarder:
+ days-to-keep: 3
+ - inject:
+ properties-content: |
+ GIT_CREDENTIALS_ID={git-credentials-id}
+
+ concurrent: false
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ trigger-on:
+ - patchset-created-event
+ - ref-updated-event
+ #- comment-added-contains-event:
+ # comment-contains-value: '(?i)^(Patch Set [0-9]+:)?( [\w\\+-]*)*(\n\n)?\s*(rebuild|recheck|retest|reverify)'
+ projects:
+ - project-compare-type: 'PLAIN'
+ project-pattern: 'mcp-ci/jenkins-config'
+ branches:
+ - branch-compare-type: 'PLAIN'
+ branch-pattern: '{jcasc_branch}'
+ custom-url: '* $JOB_NAME $BUILD_URL'
+ dsl: !include-raw-escape: pipelines/update-jenkins-config.groovy
+ sandbox: false
diff --git a/common/update-jenkins-jobs.yaml b/common/update-jenkins-jobs.yaml
new file mode 100644
index 0000000..fdb6a03
--- /dev/null
+++ b/common/update-jenkins-jobs.yaml
@@ -0,0 +1,58 @@
+- project:
+ name: jenkins-jobs
+ jobs:
+ - infra/update-jenkins-jobs
+
+- job-template:
+ name: 'infra.jenkins-jobs.update'
+ id: infra/update-jenkins-jobs
+ project-type: pipeline
+ description: |
+ <p>Update jenkins jobs configuration</p>
+ <p>Requires python-tox package and user credentials stored as JJB_USER and JJB_PASSWORD</p>
+ concurrent: false
+
+ jjb_project: 'mcp-ci/jenkins-jobs'
+
+ properties:
+ - build-discarder:
+ days-to-keep: 3
+ - inject:
+ properties-content: |
+ K8S_CLUSTER={k8s_cluster}
+ GIT_CREDENTIALS_ID={git-credentials-id}
+ JENKINS_CREDENTIALS_ID={jjb_credentials_id}
+ DOCKER_IMAGE={docker-dev-virtual}/mirantis/openstack-ci/jenkins-job-tests:latest
+ CI_NAME={ci_name}
+ SLAVE_LABEL={jjb_update_label}
+ ARTIFACTORY_URL={artifactory-url}
+ ART_CREDENTIALS_ID={artifactory_credentials_id}
+
+ parameters:
+ - bool:
+ name: MAINTAIN_MODE
+ default: false
+ description: Enable maintaining mode
+ - string:
+ name: JOBS_LIST
+ description: |
+ Space separated list of jobs to update. Will update all jobs if empty
+
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ projects:
+ - project-compare-type: PLAIN
+ project-pattern: '{jjb_project}'
+ branches:
+ - branch-pattern: 'master'
+ file-paths:
+ - compare-type: ANT
+ pattern: 'common/**'
+ - compare-type: ANT
+ pattern: 'servers/{ci_name}/**'
+ trigger-on:
+ - change-merged-event
+ custom-url: '- ${{JOB_NAME}} ${{BUILD_URL}}'
+
+ dsl: !include-raw-escape: pipelines/update-jenkins-jobs.groovy
diff --git a/conf/jenkins_job.ini.example b/conf/jenkins_job.ini.example
new file mode 100644
index 0000000..426a39e
--- /dev/null
+++ b/conf/jenkins_job.ini.example
@@ -0,0 +1,14 @@
+[jenkins]
+user=my_username
+password=my_secret_password
+url=https://my.jenkins.com/
+query_plugins_info=False
+
+[job_builder]
+ignore_cache=False
+keep_descriptions=False
+recursive=True
+include_path=.:scripts
+
+[__future__]
+param_order_from_yaml=true
diff --git a/servers/sandbox-ci/global.yaml b/servers/sandbox-ci/global.yaml
new file mode 100644
index 0000000..34b29d3
--- /dev/null
+++ b/servers/sandbox-ci/global.yaml
@@ -0,0 +1,34 @@
+- defaults:
+ name: global
+ project-type: pipeline
+ description: '{job_description}'
+ sandbox: true
+
+ ###
+ #
+ # Global variables
+ #
+ ###
+
+ job_description: Do not edit this job through the web UI!
+
+ ci_name: 'sandbox-ci'
+ jcasc_branch: '{ci_name}'
+ k8s_cluster: unset
+ jjb_update_label: unset
+ jjb_credentials_id: jjb-update
+
+ gerrit-server: 'mcp-gerrit'
+ gerrit-host: 'gerrit.mcp.mirantis.net'
+ gerrit-port: '29418'
+ gerrit-url: 'https://gerrit.mcp.mirantis.net'
+ gerrit-git-url: 'ssh://{gerrit-host}:{gerrit-port}'
+ gerrit-custom-url: '* $JOB_NAME $BUILD_URL/consoleText'
+
+ artifactory-url: 'https://artifactory.mcp.mirantis.net'
+ artifactory_credentials_id: artifactory
+
+ git-credentials-id: '{ci_name}'
+ docker-prod-virtual: 'docker-prod-virtual.docker.mirantis.net'
+ docker-dev-local: 'docker-dev-local.docker.mirantis.net'
+ docker-dev-virtual: 'docker-dev-virtual.docker.mirantis.net'
diff --git a/servers/sandbox-ci/infra/codenarc.yaml b/servers/sandbox-ci/infra/codenarc.yaml
new file mode 100644
index 0000000..cd1b985
--- /dev/null
+++ b/servers/sandbox-ci/infra/codenarc.yaml
@@ -0,0 +1,16 @@
+---
+
+- project:
+ name: codenarc
+ jobs:
+ - common/codenarc:
+ projects:
+ - project-pattern: mcp-ci/jenkins-jobs
+ branches:
+ - branch-pattern: 'master'
+ file-paths:
+ - compare-type: ANT
+ pattern: 'common/**/*.groovy'
+ - compare-type: ANT
+ pattern: 'servers/{ci_name}/**/*.groovy'
+
diff --git a/servers/sandbox-ci/infra/gerrit-projects.yaml b/servers/sandbox-ci/infra/gerrit-projects.yaml
new file mode 100644
index 0000000..d7560a9
--- /dev/null
+++ b/servers/sandbox-ci/infra/gerrit-projects.yaml
@@ -0,0 +1,56 @@
+- project:
+ name: gerrit-projects
+ tox_env:
+ - update:
+ #gerrit_event: ref-updated-event
+ #refspec: '$GERRIT_REFNAME'
+ gerrit_event: change-merged-event
+ refspec: '$GERRIT_BRANCH'
+ - check:
+ gerrit_event: patchset-created-event
+ refspec: '$GERRIT_REFSPEC'
+ jobs:
+ - infra/gerrit-projects
+
+- job-template:
+ name: infra.gerrit-projects.{tox_env}
+ id: infra/gerrit-projects
+ project-type: pipeline
+ concurrent: False
+
+ properties:
+ - build-discarder:
+ days-to-keep: 15
+ - inject:
+ properties-content: |
+ CI_NAME={ci_name}
+ TOX_ENV={tox_env}
+ DOCKER_IMAGE={docker-infra-agent}
+ K8S_CLUSTER={k8s_cluster}
+ GIT_CREDENTIALS_ID={git-credentials-id}
+ REFSPEC={refspec}
+ JEEPYB_GERRIT_HOST={gerrit-host}
+ JEEPYB_USER=mcp-gerrit
+ JEEPYB_COMMITTER=Gerrit MCP <mcp-gerrit@mirantis.com>
+ JEEPYB_CREDENTIALS_ID=gerrit
+ ARTIFACTORY_URL={artifactory-url}
+ ART_CREDENTIALS_ID={artifactory_credentials_id}
+
+ parameters:
+ - bool:
+ name: MAINTAIN_MODE
+ default: false
+ description: Enable maintaining mode
+
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ trigger-on:
+ - '{gerrit_event}'
+ projects:
+ - project-pattern: '{gerrit-projects-project}'
+ branches:
+ - branch-pattern: 'master'
+ custom-url: '* $JOB_NAME $BUILD_URL'
+
+ dsl: !include-raw-escape: pipelines/gerrit-projects.groovy
diff --git a/servers/sandbox-ci/infra/pipelines/gerrit-projects.groovy b/servers/sandbox-ci/infra/pipelines/gerrit-projects.groovy
new file mode 100644
index 0000000..3835ceb
--- /dev/null
+++ b/servers/sandbox-ci/infra/pipelines/gerrit-projects.groovy
@@ -0,0 +1,102 @@
+#!groovy
+
+def main() {
+ String gitUrl = "${env.GERRIT_SCHEME}://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ String gitRef = env.REFSPEC
+ String artInfraNamespace = 'binary-dev-local/infra'
+ String artCacheFile = "${env.CI_NAME}.jeepyb.zip"
+ String artCacheUrl = "${env.ARTIFACTORY_URL}/artifactory/${artInfraNamespace}/${artCacheFile}"
+ String artCredential = env.ART_CREDENTIALS_ID
+ String cacheHome = '.workspace'
+ def response
+
+ stage('SCM checkout') {
+ if (env.MAINTAIN_MODE.toLowerCase() == 'false') {
+ checkout \
+ $class: 'GitSCM',
+ branches: [[
+ name: 'FETCH_HEAD'
+ ]],
+ userRemoteConfigs: [[
+ url: gitUrl,
+ refspec: gitRef,
+ credentialsId: env.GIT_CREDENTIALS_ID
+ ]],
+ extensions: [[
+ $class: 'WipeWorkspace'
+ ]]
+ }
+ }
+
+ stage('Get jeepyb cache') {
+ dir(cacheHome) {
+ response = httpRequest \
+ url: artCacheUrl,
+ authentication: artCredential,
+ httpMode: 'GET',
+ outputFile: artCacheFile,
+ validResponseCodes: '100:399,404'
+ if (response.status != 404) {
+ unzip \
+ zipFile: artCacheFile
+ }
+ }
+ }
+
+ stage("Jeepyb-${TOX_ENV}") {
+ if (env.MAINTAIN_MODE.toLowerCase() == 'false') {
+ sshagent([env.JEEPYB_CREDENTIALS_ID]) {
+ withEnv([
+ "HOME=${cacheHome}",
+ 'GIT_SSH_VARIANT=ssh'
+ ]) {
+ sh 'tox -v -e ${TOX_ENV}'
+ }
+ }
+ } else {
+ input \
+ message: 'Sleeping for maintainance'
+ }
+ }
+ stage('Save jeepyb cache') {
+ dir(cacheHome) {
+ sh "rm -f ${artCacheFile}"
+ zip \
+ zipFile: artCacheFile,
+ glob: 'cache/project.cache'
+ response = httpRequest \
+ url: artCacheUrl,
+ authentication: artCredential,
+ httpMode: 'PUT',
+ multipartName: 'file',
+ uploadFile: artCacheFile
+ }
+ }
+}
+
+String podTpl = """
+ apiVersion: "v1"
+ kind: "Pod"
+ spec:
+ securityContext:
+ runAsUser: 1000
+ containers:
+ - name: "main"
+ image: "${env.DOCKER_IMAGE}"
+ command:
+ - "cat"
+ securityContext:
+ privileged: false
+ tty: true
+"""
+podTemplate(
+ cloud: env.K8S_CLUSTER,
+ yaml: podTpl,
+ showRawYaml: false
+ ) {
+ node(POD_LABEL) {
+ container('main') {
+ main()
+ }
+ }
+}
diff --git a/servers/sandbox-ci/infra/pipelines/terraform-validate.groovy b/servers/sandbox-ci/infra/pipelines/terraform-validate.groovy
new file mode 100644
index 0000000..13e0a6f
--- /dev/null
+++ b/servers/sandbox-ci/infra/pipelines/terraform-validate.groovy
@@ -0,0 +1,92 @@
+#!groovy
+
+def main() {
+ // Get & prepare source code
+ stage('SCM checkout') {
+ String gitUrl = "${env.GERRIT_SCHEME}://${env.GERRIT_HOST}:${env.GERRIT_PORT}/${env.GERRIT_PROJECT}"
+ String gitRef = env.GERRIT_REFSPEC
+
+ checkout \
+ $class: 'GitSCM',
+ branches: [[
+ name: 'FETCH_HEAD'
+ ]],
+ userRemoteConfigs: [[
+ url: gitUrl,
+ refspec: gitRef,
+ credentialsId: env.GIT_CREDENTIALS_ID
+ ]],
+ extensions: [[
+ $class: 'WipeWorkspace'
+ ]]
+ }
+
+ stage('Terraform-lint') {
+ String changedFolders = sh \
+ script: '''
+ git show \
+ --name-only \
+ --diff-filter=AM \
+ | grep '.tf\$' \
+ | awk -F'/' '{print $1"/"$2}' \
+ | uniq
+ ''',
+ returnStdout: true
+
+ Integer result = 0
+ changedFolders.tokenize('\n').each {
+ dir(it) {
+ withCredentials([
+ usernamePassword(
+ credentialsId: env.AWS_CREDENTIALS_ID,
+ usernameVariable: 'AWS_ACCESS_KEY_ID',
+ passwordVariable: 'AWS_SECRET_ACCESS_KEY'
+ )
+ ]) {
+ result += sh \
+ script: 'terraform init',
+ returnStatus: true
+ result += sh \
+ script: 'terraform validate',
+ returnStatus: true
+ result += sh \
+ script: 'terraform fmt -diff -recursive -check',
+ returnStatus: true
+ }
+ }
+ }
+ if (result > 0) {
+ currentBuild.result = 'FAILURE'
+ }
+ }
+}
+
+String podTpl = """
+ apiVersion: "v1"
+ kind: "Pod"
+ spec:
+ securityContext:
+ runAsUser: 1000
+ containers:
+ - name: "terraform-test"
+ image: "${env.DOCKER_IMAGE}"
+ command:
+ - "cat"
+ securityContext:
+ privileged: false
+ tty: true
+"""
+
+podTemplate(
+ cloud: env.K8S_CLUSTER,
+ yaml: podTpl,
+ showRawYaml: false
+ ) {
+ node(POD_LABEL) {
+ container('terraform-test') {
+ ansiColor {
+ main()
+ }
+ }
+ }
+}
diff --git a/servers/sandbox-ci/infra/shellcheck.yaml b/servers/sandbox-ci/infra/shellcheck.yaml
new file mode 100644
index 0000000..336ff85
--- /dev/null
+++ b/servers/sandbox-ci/infra/shellcheck.yaml
@@ -0,0 +1,14 @@
+---
+- project:
+ name: shellcheck
+ jobs:
+ - common/shellcheck:
+ projects:
+ - project-pattern: mcp-ci/jenkins-jobs
+ branches:
+ - branch-pattern: 'master'
+ file-paths:
+ - compare-type: ANT
+ pattern: 'common/**/*.sh'
+ - compare-type: ANT
+ pattern: 'servers/{ci_name}/**/*.sh'
diff --git a/servers/sandbox-ci/infra/terraform-validate.yaml b/servers/sandbox-ci/infra/terraform-validate.yaml
new file mode 100644
index 0000000..d79b45e
--- /dev/null
+++ b/servers/sandbox-ci/infra/terraform-validate.yaml
@@ -0,0 +1,42 @@
+---
+- project:
+ name: terraform-validate
+ jobs:
+ - infra/terraform-validate
+
+- job-template:
+ name: infra.terraform-validate
+ id: infra/terraform-validate
+ project-type: pipeline
+ description: Check terraform files by terraform validate and terraform fmt
+ concurrent: true
+
+ properties:
+ - build-discarder:
+ days-to-keep: 14
+ - inject:
+ properties-content: |
+ DOCKER_IMAGE={docker-dev-virtual}/mirantis/openstack-ci/jenkins-job-tests:latest
+ GIT_CREDENTIALS_ID={git-credentials-id}
+ AWS_CREDENTIALS_ID={aws-credentials-id}
+
+ triggers:
+ - gerrit:
+ server-name: '{gerrit-server}'
+ projects:
+ - project-compare-type: PLAIN
+ project-pattern: mcp-ci/infra
+ branches:
+ - branch-pattern: 'master'
+ file-paths:
+ - compare-type: ANT
+ pattern: 'terraform/**'
+ trigger-on:
+ - patchset-created-event:
+ exclude-drafts: True
+ custom-url: '- ${{JOB_NAME}} ${{BUILD_URL}}console'
+ skip-vote:
+ failed: false
+ notbuilt: false
+
+ dsl: !include-raw-escape: pipelines/terraform-validate.groovy
diff --git a/servers/sandbox-ci/infra/tox.yaml b/servers/sandbox-ci/infra/tox.yaml
new file mode 100644
index 0000000..78e3270
--- /dev/null
+++ b/servers/sandbox-ci/infra/tox.yaml
@@ -0,0 +1,16 @@
+---
+- project:
+ name: tox
+
+ jobs:
+ - common/tox:
+ projects:
+ - project-compare-type: PLAIN
+ project-pattern: mcp-ci/infra
+ branches:
+ - branch-compare-type: PLAIN
+ branch-pattern: master
+ skip_vote:
+ failed: false
+ notbuilt: true
+
diff --git a/servers/sandbox-ci/infra/yamllint.yaml b/servers/sandbox-ci/infra/yamllint.yaml
new file mode 100644
index 0000000..fdf0ac1
--- /dev/null
+++ b/servers/sandbox-ci/infra/yamllint.yaml
@@ -0,0 +1,20 @@
+---
+- project:
+ name: yamllint
+ yaml_paths:
+ - compare-type: REG_EXP
+ pattern: '.*\.ya?ml$'
+
+ jobs:
+ - common/yamllint:
+ projects:
+ - project-compare-type: PLAIN
+ project-pattern: mcp-ci/jenkins-config
+ branches:
+ - branch-compare-type: PLAIN
+ branch-pattern: '{ci_name}'
+ file-paths: '{obj:yaml_paths}'
+ skip_vote:
+ failed: false
+ notbuilt: true
+
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..f89ded6
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,62 @@
+[tox]
+minversion = 1.6
+skipsdist = True
+envlist = jobs
+
+[testenv]
+usedevelop = False
+envdir = {toxworkdir}/shared
+deps =
+ git+https://opendev.org/jjb/jenkins-job-builder@3.0.2#egg=jenkins-job-builder
+passenv = JENKINS_URL JJB_* CI_NAME HOME XDG_CACHE_HOME
+whitelist_externals =
+ cp
+ sed
+
+[testenv:jobs]
+commands =
+ jenkins-jobs --version
+ jenkins-jobs \
+ --conf conf/jenkins_job.ini.example \
+ test servers/sandbox-ci:common \
+ -o {toxinidir}/output/sandbox-ci
+
+[testenv:update]
+commands =
+ cp {toxinidir}/conf/jenkins_job.ini.example {toxinidir}/conf/jenkins_job.ini~
+ sed -i "s|^url=.*|url={env:JENKINS_URL:unknown}|" {toxinidir}/conf/jenkins_job.ini~
+ jenkins-jobs --version
+ jenkins-jobs \
+ --conf {toxinidir}/conf/jenkins_job.ini~ \
+ update \
+ --jobs-only \
+ servers/sandbox-ci:common {posargs}
+
+[testenv:compare-xml-old]
+commands =
+ jenkins-jobs --version
+ jenkins-jobs \
+ --conf conf/jenkins_job.ini.example \
+ test \
+ servers/sandbox-ci:common \
+ -o {toxinidir}/output/sandbox-ci/old
+
+[testenv:compare-xml-new]
+commands =
+ jenkins-jobs --version
+ jenkins-jobs \
+ --conf conf/jenkins_job.ini.example \
+ test \
+ servers/sandbox-ci:common \
+ -o {toxinidir}/output/sandbox-ci/new
+
+[testenv:delete]
+commands =
+ cp {toxinidir}/conf/jenkins_job.ini.example {toxinidir}/conf/jenkins_job.ini~
+ sed -i "s|^url=.*|url={env:JENKINS_URL:unknown}|" {toxinidir}/conf/jenkins_job.ini~
+ jenkins-jobs --version
+ jenkins-jobs \
+ --conf {toxinidir}/conf/jenkins_job.ini~ \
+ delete \
+ --jobs-only \
+ {posargs}