Initial commit
Add infrastructure jobs for sandbox

Related-PROD: RE-336

Change-Id: I2140d47e3fc360ab05f92175b29b31e69b2ec10b
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()
+            }
+        }
+    }
+}