Merge "Meridio: Images renamed, scanner, error handler"
[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-ubuntu1804') {
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 git_project = params.GIT_PROJECT
38         def current_branch = params.CURRENT_BRANCH
39         def default_branch = params.DEFAULT_BRANCH
40         def build_steps = params.BUILD_STEPS
41         def image_registry = params.IMAGE_REGISTRY
42         def local_version =  "${env.JOB_NAME}-${build_number}"
43
44         timeout(30) {
45             stage('Clone/Checkout') {
46                 git branch: default_branch, url: git_project
47                 checkout([
48                     $class: 'GitSCM',
49                     branches: [[name: current_branch]],
50                     extensions: [],
51                     userRemoteConfigs: [[
52                         refspec: '+refs/pull/*/head:refs/remotes/origin/pr/*',
53                         url: git_project
54                     ]]
55                 ])
56                 sh 'git show'
57             }
58             stage('Verify') {
59                 Verify().call()
60             }
61             stage('Docker login') {
62                 withCredentials([usernamePassword(credentialsId: 'nordix-cicd-harbor-credentials', passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {
63                     sh '''#!/bin/bash -eu
64                     echo $HARBOR_PASSWORD | docker login --username $HARBOR_USERNAME --password-stdin $IMAGE_REGISTRY
65                     '''
66                 }
67             }
68             stage('Base Image') {
69                 BaseImage(version, build_steps, image_registry, local_version).call()
70             }
71             stage('Images') {
72                 Images(image_names, version, build_steps, image_registry, local_version).call()
73             }
74             stage('E2E') {
75                 E2e(e2e_enabled).call()
76             }
77         }
78         stage('Cleanup') {
79             Cleanup()
80         }
81     }
82 }
83
84 // Static analysis: Runs the GeneratedCode function and then UnitTests and Linter in parallel
85 def Verify() {
86     return {
87         GeneratedCode().call() // cannot generate code and run the linter and tests at the same time
88         // Linter().call()
89         // UnitTests().call()
90         def stages = [:]
91         stages.put('Unit Tests', UnitTests())
92         stages.put('Linter', Linter())
93         // stages.put('Generated code verification', GeneratedCode())
94         parallel(stages)
95     }
96 }
97
98 // Runs the unit tests and set the github commit status
99 def UnitTests() {
100     return {
101         def context = 'Unit Tests'
102         stage('Unit Tests') {
103             def command = 'make test'
104             try {
105                 SetBuildStatus(in_progress, context, pending)
106                 ExecSh(command).call()
107                 SetBuildStatus(completed, context, success)
108             } catch (Exception e) {
109                 SetBuildStatus(failed, context, failure)
110                 Error(exception_message_exec + command).call()
111             }
112         }
113     }
114 }
115
116 // Runs the linter and set the github commit status
117 def Linter() {
118     return {
119         def context = 'Linter'
120         stage('Linter') {
121             def command = 'make lint'
122             try {
123                 SetBuildStatus(in_progress, context, pending)
124                 ExecSh(command).call()
125                 SetBuildStatus(completed, context, success)
126             } catch (Exception e) {
127                 SetBuildStatus(failed, context, failure)
128                 Error(exception_message_exec + command).call()
129             }
130         }
131     }
132 }
133
134 // Check if code has been generated correctly and set the github commit status:
135 // go.mod: runs "go mod tidy"
136 // go generate ./...: Code should be generated using "make genrate" command
137 // proto: skipped due to version of protoc
138 // If files are generated correctly then GetModifiedFiles function should return an empty string
139 def GeneratedCode() {
140     return {
141         def context = 'Generated code verification'
142         SetBuildStatus(in_progress, context, pending)
143         stage('go mod tidy') {
144             def command = 'go mod tidy'
145             try {
146                 ExecSh(command).call()
147                 if (GetModifiedFiles() != '') {
148                     throw new Exception(exception_message_code_generation)
149                 }
150             } catch (Exception e) {
151                 SetBuildStatus(failed, context, failure)
152                 Error(exception_message_exec + command).call()
153             }
154         }
155         stage('go generate ./...') {
156             def command = 'make generate'
157             try {
158                 ExecSh(command).call()
159                 if (GetModifiedFiles() != '') {
160                     throw new Exception(exception_message_code_generation)
161                 }
162             } catch (Exception e) {
163                 SetBuildStatus(failed, context, failure)
164                 Error(exception_message_exec + command).call()
165             }
166         }
167         stage('Proto') {
168             // TODO: protoc version could be different
169             Utils.markStageSkippedForConditional('Proto')
170         // try {
171         //     sh 'make proto'
172         //     if (GetModifiedFiles() != '') {
173         //         throw new Exception(exception_message)
174         //     }
175         // } catch (Exception e) {
176         //     SetBuildStatus(failed, context, failure)
177         //     sh 'git diff'
178         //     sh 'git status -s'
179         //     Error(e).call()
180         // }
181         }
182         SetBuildStatus(completed, context, success)
183     }
184 }
185
186 def BaseImage(version, build_steps, registry, local_version) {
187     return {
188         Build(base_image, version, build_steps, registry, local_version).call()
189     }
190 }
191
192 // Call Build function for every images in parallel
193 def Images(images, version, build_steps, registry, local_version) {
194     return {
195         def stages = [:]
196         for (i in images) {
197             stages.put(i, Build(i, version, build_steps, registry, local_version))
198         }
199         parallel(stages)
200     }
201 }
202
203 // Build set the github commit status
204 def Build(image, version, build_steps, registry, local_version) {
205     return {
206         stage("${image} (${version}): ${build_steps}") {
207             def context = "Image: ${image}"
208             def in_progress_message = "${in_progress} (${build_steps})"
209             def completed_message = "${completed} (${build_steps})"
210             def failed_message = "${failed} (${build_steps})"
211             def command = "make ${image} VERSION=${version} BUILD_STEPS='${build_steps}' REGISTRY=${registry} LOCAL_VERSION=${local_version} BASE_IMAGE=${base_image}:${local_version}"
212             try {
213                 SetBuildStatus(in_progress_message, context, pending)
214                 ExecSh(command).call()
215                 SetBuildStatus(completed_message, context, success)
216             } catch (Exception e) {
217                 SetBuildStatus(failed_message, context, failure)
218                 Error(exception_message_exec + command).call()
219             }
220         }
221     }
222 }
223
224 // Run the E2e Tests
225 // Currently skipped
226 def E2e(e2e_enabled) {
227     if (e2e_enabled == 'true') {
228         return {
229             echo 'make e2e' // todo
230         }
231     } else {
232         return {
233             Utils.markStageSkippedForConditional('E2E')
234         }
235     }
236 }
237
238 // Raise error in Jenkins job
239 def Error(e) {
240     return {
241         sh 'git diff'
242         sh 'git status -s'
243         Cleanup()
244         error e
245     }
246 }
247
248 // Cleanup directory
249 def Cleanup() {
250     cleanWs()
251 }
252
253 // Execute command
254 def ExecSh(command) {
255     return {
256         sh """
257             . \${HOME}/.profile
258             ${command}
259         """
260     }
261 }
262
263 // Set the commit status on Github
264 // https://plugins.jenkins.io/github/#plugin-content-pipeline-examples
265 def SetBuildStatus(String message, String context, String state) {
266     step([
267         $class: 'GitHubCommitStatusSetter',
268         reposSource: [$class: 'ManuallyEnteredRepositorySource', url: 'https://github.com/Nordix/Meridio'],
269         commitShaSource: [$class: 'ManuallyEnteredShaSource', sha: GetCommitSha()],
270         contextSource: [$class: 'ManuallyEnteredCommitContextSource', context: context],
271         errorHandlers: [[$class: 'ShallowAnyErrorHandler']], // Prevent GitHubCommitStatusSetter to set the job status to unstable
272         statusResultSource: [ $class: 'ConditionalStatusResultSource', results: [[$class: 'AnyBuildResult', message: message, state: state]] ]
273   ])
274 }
275
276 // Return the current commit sha
277 def GetCommitSha() {
278     return sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
279 }
280
281 // Returns if any files has been modified/added/removed
282 def GetModifiedFiles() {
283     return sh(script: 'git status -s', returnStdout: true).trim()
284 }