/* Copyright (c) 2022 Nordix Foundation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import org.jenkinsci.plugins.pipeline.modeldefinition.Utils pending = 'PENDING' success = 'SUCCESS' failure = 'FAILURE' base_image = 'base-image' in_progress = 'In Progress.' completed = 'Completed.' failed = 'Failed' exception_message_exec = 'failed to execute the following command: ' exception_message_code_generation = 'Generated code verification failed' node('nordix-nsm-build-ubuntu2204') { build_number = env.BUILD_NUMBER workspace = env.WORKSPACE ws("${workspace}/${build_number}") { def image_names = params.IMAGE_NAMES.split(' ') def version = params.IMAGE_VERSION def e2e_enabled = params.E2E_ENABLED def helm_chart_upload = params.HELM_CHART_UPLOAD def security_scan_enabled = params.SECURITY_SCAN_ENABLED def git_project = params.GIT_PROJECT def current_branch = params.CURRENT_BRANCH def default_branch = params.DEFAULT_BRANCH def build_steps = params.BUILD_STEPS def image_registry = params.IMAGE_REGISTRY def local_version = "${env.JOB_NAME}-${build_number}" timeout(30) { stage('Clone/Checkout') { git branch: default_branch, url: git_project checkout([ $class: 'GitSCM', branches: [[name: current_branch]], extensions: [], userRemoteConfigs: [[ refspec: '+refs/pull/*/head:refs/remotes/origin/pr/*', url: git_project ]] ]) sh 'git show' } Verify().call() stage('Docker login') { if (env.DRY_RUN != 'true') { withCredentials([usernamePassword(credentialsId: 'nordix-cicd-harbor-credentials', passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) { sh '''#!/bin/bash -eu echo $HARBOR_PASSWORD | docker login --username $HARBOR_USERNAME --password-stdin $IMAGE_REGISTRY ''' } } else { echo 'Docker login' } } stage('Base Image') { BaseImage(version, build_steps, image_registry, local_version).call() } stage('Images') { Images(image_names, version, build_steps, image_registry, local_version).call() if (currentBuild.result == 'FAILURE') { Error('Failed to build image(s)').call() } } stage('Helm Chart') { HelmChart(helm_chart_upload, version).call() } stage('Security Scan') { if (security_scan_enabled == true) { SecurityScan(current_branch, version).call() } else { Utils.markStageSkippedForConditional('Security Scan') } } stage('E2E') { if (e2e_enabled == true) { E2e(current_branch, version).call() } else { Utils.markStageSkippedForConditional('E2E') } } } stage('Cleanup') { Cleanup() } } } // Verify the Generated code, UnitTests and Linter def Verify() { return { GeneratedCode().call() // cannot generate code and run the linter and tests at the same time Linter().call() UnitTests().call() } } // Runs the unit tests and set the github commit status def UnitTests() { return { def context = 'Unit Tests' stage('Unit Tests') { def command = 'make test' try { SetBuildStatus(in_progress, context, pending) ExecSh(command).call() SetBuildStatus(completed, context, success) } catch (Exception e) { SetBuildStatus(failed, context, failure) Error("${exception_message_exec} ${command}").call() } } } } // Runs the linter and set the github commit status def Linter() { return { def context = 'Linter' stage('Linter') { def command = 'make lint' try { SetBuildStatus(in_progress, context, pending) ExecSh(command).call() SetBuildStatus(completed, context, success) } catch (Exception e) { SetBuildStatus(failed, context, failure) Error("${exception_message_exec} ${command}").call() } } } } // Check if code has been generated correctly and set the github commit status: // go.mod: runs "go mod tidy" // go generate ./...: Code should be generated using "make genrate" command // proto: skipped due to version of protoc // If files are generated correctly then GetModifiedFiles function should return an empty string def GeneratedCode() { return { def context = 'Generated code verification' stage('Generated code verification') { def command = 'make go-generate manifests generate-controller' try { SetBuildStatus(in_progress, context, pending) ExecSh(command).call() if (GetModifiedFiles() != '') { throw new Exception(exception_message_code_generation) } SetBuildStatus(completed, context, success) } catch (Exception e) { SetBuildStatus(failed, context, failure) Error(exception_message_exec + command).call() } } } } def BaseImage(version, build_steps, registry, local_version) { return { Build(base_image, version, build_steps, registry, local_version).call() } } // Call Build function for every images in parallel def Images(images, version, build_steps, registry, local_version) { return { def stages = [:] for (i in images) { stages.put(i, Build(i, version, build_steps, registry, local_version)) } parallel(stages) } } // Build set the github commit status def Build(image, version, build_steps, registry, local_version) { return { stage("${image} (${version}): ${build_steps}") { def context = "Image: ${image}" def in_progress_message = "${in_progress} (${build_steps})" def completed_message = "${completed} (${build_steps})" def failed_message = "${failed} (${build_steps})" def command = "make ${image} VERSION=${version} BUILD_STEPS='${build_steps}' REGISTRY=${registry} LOCAL_VERSION=${local_version} BASE_IMAGE=${base_image}:${local_version}" try { SetBuildStatus(in_progress_message, context, pending) ExecSh(command).call() SetBuildStatus(completed_message, context, success) } catch (Exception e) { SetBuildStatus(failed_message, context, failure) unstable "${exception_message_exec} ${command}" currentBuild.result = 'FAILURE' } } } } // Generate and upload the helm chart def HelmChart(helm_chart_upload, version) { return { parallel( 'Helm Chart': { stage('Generate Helm Chart') { def context = 'Generate Helm Chart' def command = "make generate-helm-chart VERSION=${version}" try { SetBuildStatus(in_progress, context, pending) ExecSh(command).call() SetBuildStatus(completed, context, success) } catch (Exception e) { SetBuildStatus(failed, context, failure) Error("${exception_message_exec} ${command}").call() } } stage('Upload Helm Chart') { if (helm_chart_upload == true) { withCredentials([string(credentialsId: 'nsm-nordix-artifactory-api-key', variable: 'API_KEY')]) { ExecSh(""" charts=\$(cd _output/helm/ && ls *.tgz) for chart in \$charts do curl -H 'X-JFrog-Art-Api:${API_KEY}' -T _output/helm/\$chart \"https://artifactory.nordix.org/artifactory/cloud-native/meridio/\$chart\" done """).call() } } else { Utils.markStageSkippedForConditional('Upload Helm Chart') } } } ) } } // Run the security scan job def SecurityScan(current_branch, version) { return { build job: 'meridio-periodic-security-scan', parameters: [ string(name: 'IMAGE_VERSION', value: "$version"), string(name: 'CURRENT_BRANCH', value: "$current_branch"), string(name: 'DRY_RUN', value: env.DRY_RUN) ], wait: true } } // Run the E2e Tests // Currently skipped def E2e(current_branch, version) { return { build job: 'meridio-e2e-test-kind', parameters: [ string(name: 'MERIDIO_VERSION', value: "$version"), string(name: 'TAPA_VERSION', value: "$version"), string(name: 'CURRENT_BRANCH', value: "$current_branch"), string(name: 'DRY_RUN', value: env.DRY_RUN) ], wait: true } } // Raise error in Jenkins job def Error(e) { return { sh 'git diff' sh 'git status -s' Cleanup() error e } } // Cleanup directory def Cleanup() { cleanWs() } // Execute command def ExecSh(command) { return { if (env.DRY_RUN != 'true') { sh """ . \${HOME}/.profile ${command} """ } else { echo "${command}" } } } // Set the commit status on Github // https://plugins.jenkins.io/github/#plugin-content-pipeline-examples def SetBuildStatus(String message, String context, String state) { if (env.DRY_RUN != 'true') { step([ $class: 'GitHubCommitStatusSetter', reposSource: [$class: 'ManuallyEnteredRepositorySource', url: 'https://github.com/Nordix/Meridio'], commitShaSource: [$class: 'ManuallyEnteredShaSource', sha: GetCommitSha()], contextSource: [$class: 'ManuallyEnteredCommitContextSource', context: context], errorHandlers: [[$class: 'ShallowAnyErrorHandler']], // Prevent GitHubCommitStatusSetter to set the job status to unstable statusResultSource: [ $class: 'ConditionalStatusResultSource', results: [[$class: 'AnyBuildResult', message: message, state: state]] ] ]) } } // Return the current commit sha def GetCommitSha() { return sh(script: 'git rev-parse HEAD', returnStdout: true).trim() } // Returns if any files has been modified/added/removed def GetModifiedFiles() { return sh(script: 'git status -s', returnStdout: true).trim() }