Meridio: periodic job improvements
[infra/cicd.git] / jjb / nsm / Jenkinsfile
1 /*
2 Copyright (c) 2022 Nordix Foundation
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8     http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15 */
16
17 import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
18
19 pending = 'PENDING'
20 success = 'SUCCESS'
21 failure = 'FAILURE'
22 base_image = 'base-image'
23 in_progress = 'In Progress.'
24 completed = 'Completed.'
25 failed = 'Failed'
26
27 exception_message_exec = 'failed to execute the following command: '
28 exception_message_code_generation = 'Generated code verification failed'
29
30 node('nordix-nsm-build-ubuntu2204') {
31     build_number = env.BUILD_NUMBER
32     workspace = env.WORKSPACE
33     ws("${workspace}/${build_number}") {
34         def image_names = params.IMAGE_NAMES.split(' ')
35         def version = params.IMAGE_VERSION
36         def e2e_enabled = params.E2E_ENABLED
37         def helm_chart_upload = params.HELM_CHART_UPLOAD
38         def security_scan_enabled = params.SECURITY_SCAN_ENABLED
39         def git_project = params.GIT_PROJECT
40         def current_branch = params.CURRENT_BRANCH
41         def default_branch = params.DEFAULT_BRANCH
42         def build_steps = params.BUILD_STEPS
43         def image_registry = params.IMAGE_REGISTRY
44         def local_version =  "${env.JOB_NAME}-${build_number}"
45
46         timeout(30) {
47             stage('Clone/Checkout') {
48                 git branch: default_branch, url: git_project
49                 checkout([
50                     $class: 'GitSCM',
51                     branches: [[name: current_branch]],
52                     extensions: [],
53                     userRemoteConfigs: [[
54                         refspec: '+refs/pull/*/head:refs/remotes/origin/pr/*',
55                         url: git_project
56                     ]]
57                 ])
58                 sh 'git show'
59             }
60             Verify().call()
61             stage('Docker login') {
62                 if (env.DRY_RUN != 'true') {
63                     withCredentials([usernamePassword(credentialsId: 'nordix-cicd-harbor-credentials', passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {
64                         sh '''#!/bin/bash -eu
65                     echo $HARBOR_PASSWORD | docker login --username $HARBOR_USERNAME --password-stdin $IMAGE_REGISTRY
66                     '''
67                     }
68                 } else {
69                     echo 'Docker login'
70                 }
71             }
72             stage('Base Image') {
73                 BaseImage(version, build_steps, image_registry, local_version).call()
74             }
75             stage('Images') {
76                 Images(image_names, version, build_steps, image_registry, local_version).call()
77                 if (currentBuild.result == 'FAILURE') {
78                     Error('Failed to build image(s)').call()
79                 }
80             }
81             stage('Helm Chart') {
82                 HelmChart(helm_chart_upload, version).call()
83             }
84             stage('Security Scan') {
85                 if (security_scan_enabled == true) {
86                     SecurityScan(current_branch, version).call()
87                 } else {
88                     Utils.markStageSkippedForConditional('Security Scan')
89                 }
90             }
91             stage('E2E') {
92                 if (e2e_enabled == true) {
93                     E2e(current_branch, version).call()
94                 } else {
95                     Utils.markStageSkippedForConditional('E2E')
96                 }
97             }
98         }
99         stage('Cleanup') {
100             Cleanup()
101         }
102     }
103 }
104
105 // Verify the Generated code, UnitTests and Linter
106 def Verify() {
107     return {
108         GeneratedCode().call() // cannot generate code and run the linter and tests at the same time
109         Linter().call()
110         UnitTests().call()
111     }
112 }
113
114 // Runs the unit tests and set the github commit status
115 def UnitTests() {
116     return {
117         def context = 'Unit Tests'
118         stage('Unit Tests') {
119             def command = 'make test'
120             try {
121                 SetBuildStatus(in_progress, context, pending)
122                 ExecSh(command).call()
123                 SetBuildStatus(completed, context, success)
124             } catch (Exception e) {
125                 SetBuildStatus(failed, context, failure)
126                 Error("${exception_message_exec} ${command}").call()
127             }
128         }
129     }
130 }
131
132 // Runs the linter and set the github commit status
133 def Linter() {
134     return {
135         def context = 'Linter'
136         stage('Linter') {
137             def command = 'make lint'
138             try {
139                 SetBuildStatus(in_progress, context, pending)
140                 ExecSh(command).call()
141                 SetBuildStatus(completed, context, success)
142             } catch (Exception e) {
143                 SetBuildStatus(failed, context, failure)
144                 Error("${exception_message_exec} ${command}").call()
145             }
146         }
147     }
148 }
149
150 // Check if code has been generated correctly and set the github commit status:
151 // go.mod: runs "go mod tidy"
152 // go generate ./...: Code should be generated using "make genrate" command
153 // proto: skipped due to version of protoc
154 // If files are generated correctly then GetModifiedFiles function should return an empty string
155 def GeneratedCode() {
156     return {
157         def context = 'Generated code verification'
158         stage('Generated code verification') {
159             def command = 'make go-generate manifests generate-controller'
160             try {
161                 SetBuildStatus(in_progress, context, pending)
162                 ExecSh(command).call()
163                 if (GetModifiedFiles() != '') {
164                     throw new Exception(exception_message_code_generation)
165                 }
166                 SetBuildStatus(completed, context, success)
167             } catch (Exception e) {
168                 SetBuildStatus(failed, context, failure)
169                 Error(exception_message_exec + command).call()
170             }
171         }
172     }
173 }
174
175 def BaseImage(version, build_steps, registry, local_version) {
176     return {
177         Build(base_image, version, build_steps, registry, local_version).call()
178     }
179 }
180
181 // Call Build function for every images in parallel
182 def Images(images, version, build_steps, registry, local_version) {
183     return {
184         def stages = [:]
185         for (i in images) {
186             stages.put(i, Build(i, version, build_steps, registry, local_version))
187         }
188         parallel(stages)
189     }
190 }
191
192 // Build set the github commit status
193 def Build(image, version, build_steps, registry, local_version) {
194     return {
195         stage("${image} (${version}): ${build_steps}") {
196             def context = "Image: ${image}"
197             def in_progress_message = "${in_progress} (${build_steps})"
198             def completed_message = "${completed} (${build_steps})"
199             def failed_message = "${failed} (${build_steps})"
200             def command = "make ${image} VERSION=${version} BUILD_STEPS='${build_steps}' REGISTRY=${registry} LOCAL_VERSION=${local_version} BASE_IMAGE=${base_image}:${local_version}"
201             try {
202                 SetBuildStatus(in_progress_message, context, pending)
203                 ExecSh(command).call()
204                 SetBuildStatus(completed_message, context, success)
205             } catch (Exception e) {
206                 SetBuildStatus(failed_message, context, failure)
207                 unstable "${exception_message_exec} ${command}"
208                 currentBuild.result = 'FAILURE'
209             }
210         }
211     }
212 }
213
214 // Generate and upload the helm chart
215 def HelmChart(helm_chart_upload, version) {
216     return {
217         parallel(
218             'Helm Chart': {
219                 stage('Generate Helm Chart') {
220                     def context = 'Generate Helm Chart'
221                     def command = "make generate-helm-chart VERSION=${version}"
222                     try {
223                         SetBuildStatus(in_progress, context, pending)
224                         ExecSh(command).call()
225                         SetBuildStatus(completed, context, success)
226                     } catch (Exception e) {
227                         SetBuildStatus(failed, context, failure)
228                         Error("${exception_message_exec} ${command}").call()
229                     }
230                 }
231                 stage('Upload Helm Chart') {
232                     if (helm_chart_upload == true) {
233                         withCredentials([string(credentialsId: 'nsm-nordix-artifactory-api-key', variable: 'API_KEY')]) {
234                             ExecSh("""
235                                 charts=\$(cd _output/helm/ && ls *.tgz)
236                                 for chart in \$charts
237                                 do
238                                     curl -H 'X-JFrog-Art-Api:${API_KEY}' -T _output/helm/\$chart \"https://artifactory.nordix.org/artifactory/cloud-native/meridio/\$chart\"
239                                 done
240                             """).call()
241                         }
242                     } else {
243                         Utils.markStageSkippedForConditional('Upload Helm Chart')
244                     }
245                 }
246             }
247         )
248     }
249 }
250
251 // Run the security scan job
252 def SecurityScan(current_branch, version) {
253     return {
254         build job: 'meridio-periodic-security-scan', parameters: [
255             string(name: 'IMAGE_VERSION', value: "$version"),
256             string(name: 'CURRENT_BRANCH', value: "$current_branch"),
257             string(name: 'DRY_RUN', value: env.DRY_RUN)
258         ], wait: true
259     }
260 }
261
262 // Run the E2e Tests
263 // Currently skipped
264 def E2e(current_branch, version) {
265     return {
266         build job: 'meridio-e2e-test-kind', parameters: [
267             string(name: 'MERIDIO_VERSION', value: "$version"),
268             string(name: 'TAPA_VERSION', value: "$version"),
269             string(name: 'CURRENT_BRANCH', value: "$current_branch"),
270             string(name: 'DRY_RUN', value: env.DRY_RUN)
271         ], wait: true
272     }
273 }
274
275 // Raise error in Jenkins job
276 def Error(e) {
277     return {
278         sh 'git diff'
279         sh 'git status -s'
280         Cleanup()
281         error e
282     }
283 }
284
285 // Cleanup directory
286 def Cleanup() {
287     cleanWs()
288 }
289
290 // Execute command
291 def ExecSh(command) {
292     return {
293         if (env.DRY_RUN != 'true') {
294             sh """
295                 . \${HOME}/.profile
296                 ${command}
297             """
298         } else {
299             echo "${command}"
300         }
301     }
302 }
303
304 // Set the commit status on Github
305 // https://plugins.jenkins.io/github/#plugin-content-pipeline-examples
306 def SetBuildStatus(String message, String context, String state) {
307     if (env.DRY_RUN != 'true') {
308         step([
309             $class: 'GitHubCommitStatusSetter',
310             reposSource: [$class: 'ManuallyEnteredRepositorySource', url: 'https://github.com/Nordix/Meridio'],
311             commitShaSource: [$class: 'ManuallyEnteredShaSource', sha: GetCommitSha()],
312             contextSource: [$class: 'ManuallyEnteredCommitContextSource', context: context],
313             errorHandlers: [[$class: 'ShallowAnyErrorHandler']], // Prevent GitHubCommitStatusSetter to set the job status to unstable
314             statusResultSource: [ $class: 'ConditionalStatusResultSource', results: [[$class: 'AnyBuildResult', message: message, state: state]] ]
315         ])
316     }
317 }
318
319 // Return the current commit sha
320 def GetCommitSha() {
321     return sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
322 }
323
324 // Returns if any files has been modified/added/removed
325 def GetModifiedFiles() {
326     return sh(script: 'git status -s', returnStdout: true).trim()
327 }