-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathJenkinsfile
373 lines (331 loc) · 22.3 KB
/
Jenkinsfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#!groovy
// <Important Note>
// --------------------------------------------------------------------------------------------------------------------------
// In order to use this file, the following script approval signatures will be required:
// - field java.lang.String value
// - new java.lang.Exception java.lang.String
// After running the job, in Jenkins go to Manage Jenkins -> In-Process Script Approval.
// Then approve these pending script approvals. This is due to the security system Jenkins
// is using. There are alternatives to this but the above is the most secure.
// --------------------------------------------------------------------------------------------------------------------------
// </Important Note>
// <Global Variables>
// --------------------------------------------------------------------------------------------------------------------------
nodeMap = [ // Map of server names and their IP addresses. These servers
"spacely-engineering-vm-004": "10.0.0.7", // are dedicated to running ephemeral Jenkins Slave
"spacely-engineering-vm-005": "10.0.0.8" // containers to get work done.
]
chosenNode = "" // Variable which will later store the chosen server to
// perform the job. Leave this variable empty.
// --------------------------------------------------------------------------------------------------------------------------
// </Global Variables>
// <Environment Variables>
// --------------------------------------------------------------------------------------------------------------------------
// Assign values to each of these environment variables to define your environment. The rest of the file will use these
// variables to get things done.
// --------------------------------------------------------------------------------------------------------------------------
// Private Docker Registry FQDN used for logins, pulls, pushes, etc. Be sure to omit https as it will be implicitly added.
env.PRIVATE_DOCKER_REGISTRY_FQDN = "your-private-registry.example.com"
// Private Docker Registry credentials ID which translate to the username and password used to authenticate with the registry.
// These credentials are created in Jenkins and the resulting ID obtained and placed here.
env.PRIVATE_DOCKER_REGISTRY_CREDENTIALS_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
// Docker Compose file used only by CICD for builds, tests, artifact deployments, etc. but not image deployments. All the
// relevant details on the image will be here.
env.DOCKER_COMPOSE_CICD_FILE_NAME_AND_LOCATION = "docker-compose-cicd.yml"
// Name of service defined in Docker Compose files (e.g. spring-boot-demo). Ensure this is very explicit and unique.
env.DOCKER_SERVICE_NAME = "spring-boot-demo"
// <----------------------- <Dev> ------------------------>
// Docker Compose file used for development deployments. All the relevant details on the image will be here.
env.DOCKER_COMPOSE_DEV_FILE_NAME_AND_LOCATION = "docker-compose-dev.yml"
// <----------------------- </Dev> ----------------------->
// <----------------------- <Prod> ----------------------->
// Docker Compose file used for production deployments. All the relevant details on the image will be here.
env.DOCKER_COMPOSE_PROD_FILE_NAME_AND_LOCATION = "docker-compose-prod.yml"
// <----------------------- </Prod> ---------------------->
// <----------------------- <Git Data> ----------------------->
env.GIT_UPSTREAM_REPO_URL = "https://gitlab-spacely-engineering.example.com:51443/cicd-demos/spring-boot.git"
// only set full URL if GitLab action exists, otherwise env.gitlabSourceNamespace won't exist
if (env.gitlabActionType == null) {
env.GIT_FORKED_REPO_URL = ""
} else {
// This is the forked upstream repository URL. It is important to follow the below format or merge request processing may fail.
// Do not remove the ${gitlabSourceNamespace} environment variable. It will fill in important information at runtime.
env.GIT_FORKED_REPO_URL = "https://gitlab-spacely-engineering.example.com:51443/${gitlabSourceNamespace}/spring-boot.git"
}
env.GIT_DEV_BRANCH_NAME = "develop" // Name of the development branch where all work is pushed.
// Following this approach prevents working directly out of
// the master branch which is a bad practice. The work that is
// pushed to the development branch happens through merge
// requests. See below for the logic that handles events
// for the development branch.
env.GIT_MASTER_BRANCH_NAME = "master" // The name of the master branch. No work should ever occur
// directly in the branch. However, when a release is ready
// the data from the development branch can be pulled into
// the master branch, a release created and tagged, etc.
// When this occurs a job will run. See below for the logic
// that handles events for the master branch.
env.GIT_CREDENTIALS_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" // Credentials for accessing the repository to perform
// Git operations (e.g. pull, push, etc.). These credentials
// are created in Jenkins and the resulting ID obtained and
// placed here.
env.CICD_ADMIN_NAME = "CICD Admin" // Name and email address used by Git to perform operations.
env.CICD_ADMIN_EMAIL = "[email protected]"
// <----------------------- </Git Data> ----------------------->
// --------------------------------------------------------------------------------------------------------------------------
// </Environment Variables>
// <Functions>
// --------------------------------------------------------------------------------------------------------------------------
// Prints important environment information depending upon the context of the event (e.g. push, merge, forced build, etc.).
def printEnvDetails() {
// only print environment details if a GitLab action is defined
if (env.gitlabActionType != null) {
echo "-----------------------------------------------------"
echo "GitLab Branch: ${gitlabBranch}"
echo "GitLab Source Branch: ${gitlabSourceBranch}"
echo "GitLab Action Type: ${gitlabActionType}"
echo "GitLab Username: ${gitlabUserName}"
echo "GitLab User Email: ${gitlabUserEmail}"
echo "GitLab Source Repo Homepage: ${gitlabSourceRepoHomepage}"
echo "GitLab Source Repo Name: ${gitlabSourceRepoName}"
echo "GitLab Source Namespace: ${gitlabSourceNamespace}"
echo "GitLab Source Repo URL: ${gitlabSourceRepoURL}"
echo "GitLab Source Repo SSH URL: ${gitlabSourceRepoSshUrl}"
echo "GitLab Source Repo HTTP URL: ${gitlabSourceRepoHttpUrl}"
// only print these variables when a merge action occurs - this prevents a groovy.lang.MissingPropertyException
if (env.gitlabActionType == "MERGE") {
echo "GitLab Merge Request Title: ${gitlabMergeRequestTitle}"
echo "GitLab Merge Request ID: ${gitlabMergeRequestId}"
echo "GitLab Merge Request State: ${env.gitlabMergeRequestState}"
echo "GitLab Merge Request Last Commit: ${gitlabMergeRequestLastCommit}"
echo "GitLab Merge Request Target Project ID: ${gitlabMergeRequestTargetProjectId}"
echo "GitLab Target Branch: ${gitlabTargetBranch}"
echo "GitLab Target Repo Name: ${gitlabTargetRepoName}"
echo "GitLab Target Namespace: ${gitlabTargetNamespace}"
echo "GitLab Target Repo SSH URL: ${gitlabTargetRepoSshUrl}"
echo "GitLab Target Repo HTTP URL: ${gitlabTargetRepoHttpUrl}"
}
echo "-----------------------------------------------------"
}
}
// --------------------------------------------------------------------------------------------------------------------------
// </Functions>
// <Stages, Steps, Etc.>
// --------------------------------------------------------------------------------------------------------------------------
// The master node (Jenkins Master) will be used to perform health checks and load balancing logic. It will loop through the
// map of nodes (servers used to process jobs) and check their exposed Hello World service running on port 80. If a response
// code of 200 is received, the server is considered to be available. Otherwise, it is determined to be down. Based on the
// created list of online servers, one will be chosen at random to run the ephemeral Jenkins Slave container to perform the
// job.
//
// The reason this logic must happen in the master node is because it cannot run on a node that may not be online. This logic
// is very lightweight and will not stress the master node.
node('master') {
stage("get-node") {
String statusCode = ""
List onlineNodes = []
// loop through each node and determine which is online.
nodeMap.each {
statusCode = sh(script: "curl --connect-timeout 5 -LI http://${it.value} -o /dev/null -w '%{http_code}\n' -s || true",
returnStdout: true).trim()
// if the node is online, add it to the list
if (statusCode == "200") {
onlineNodes.add(it.key)
} else {
echo "${it.key} is currently offline and unable to process jobs."
}
}
// if no available nodes are online, throw an error
if (onlineNodes.size() == 0) {
throw new Exception("No available nodes are online to process Jenkins jobs.") as java.lang.Throwable
}
// otherwise randomly choose an online node and use that to run the job (logic below)
else {
echo "${onlineNodes.size()} node(s) online and available to process Jenkins jobs."
int chosenNodeIndex = new Random().nextInt(onlineNodes.size())
String randomOnlineNode = onlineNodes.get(chosenNodeIndex)
chosenNode = randomOnlineNode
echo "Using ${randomOnlineNode} to process the Jenkins job. Please wait while container is created."
echo "Please disregard any \"Jenkins doesn’t have label\" messages."
}
}
}
// Use the chosen node to perform the job.
node (chosenNode) {
try {
// ensure timestamps appear in the logs
timestamps {
// wrap all stages with logic which will properly inform GitLab about what is going on
gitlabBuilds(builds: ["print-info", "checkout", "lint", "build", "test", "push", "deploy", "cleanup"]) {
stage("print-info") {
// wrap logic for this specific stage to keep GitLab informed
gitlabCommitStatus("print-info") {
printEnvDetails()
}
}
stage("checkout") {
// wrap logic for this specific stage to keep GitLab informed
gitlabCommitStatus("checkout") {
// if GitLab merge action occurs
if (env.gitlabActionType == "MERGE") {
// checkout forked source branch and then merge with upstream target branch
echo "Checking out upstream branch and merging with forked source branch..."
checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: 'merge-requests/${gitlabMergeRequestIid}']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'UserIdentity', email: env.CICD_ADMIN_EMAIL, name: env.CICD_ADMIN_NAME], [$class: 'PreBuildMerge', options: [fastForwardMode: 'FF', mergeRemote: 'origin', mergeStrategy: 'default', mergeTarget: '${gitlabTargetBranch}']]], submoduleCfg: [], userRemoteConfigs: [[credentialsId: env.GIT_CREDENTIALS_ID, name: 'origin', refspec: '+refs/heads/*:refs/remotes/origin/* +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*', url: env.GIT_UPSTREAM_REPO_URL], [credentialsId: env.GIT_CREDENTIALS_ID, name: '${gitlabSourceRepoName}', url: env.GIT_FORKED_REPO_URL]]]
}
// otherwise if GitLab action type is null, Jenkins triggered the job manually outside of GitLab, or if GitLab push action occurs
else if (env.gitlabActionType == null || env.gitlabActionType == "PUSH") {
// if the branch is null, Jenkins triggered the build manually, or if action is for the GitLab development branch
if (env.gitlabBranch == null || env.gitlabBranch == env.GIT_DEV_BRANCH_NAME) {
// checkout upstream development branch only
echo "Checking out upstream development branch..."
checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'UserIdentity', email: env.CICD_ADMIN_EMAIL, name: env.CICD_ADMIN_NAME]], submoduleCfg: [], userRemoteConfigs: [[credentialsId: env.GIT_CREDENTIALS_ID, url: env.GIT_UPSTREAM_REPO_URL]]]
}
// otherwise if action is for the GitLab master branch
else if (env.gitlabBranch == env.GIT_MASTER_BRANCH_NAME) {
// checkout upstream master branch only
echo "Checking out upstream master branch..."
//checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'UserIdentity', email: env.CICD_ADMIN_EMAIL, name: env.CICD_ADMIN_NAME]], submoduleCfg: [], userRemoteConfigs: [[credentialsId: env.GIT_CREDENTIALS_ID, url: env.GIT_UPSTREAM_REPO_URL]]]
} else {
echo "Skipping checkout since there are no steps defined for the ${env.gitlabBranch} branch."
}
} else {
echo "Skipping checkout since the GitLab action has no steps defined."
}
}
}
stage("lint") {
// wrap logic for this specific stage to keep GitLab informed
gitlabCommitStatus("lint") {
// if GitLab merge action occurs
if (env.gitlabActionType == "MERGE") {
// TODO: insert linting logic
} else {
echo "Skipping linting since the GitLab action has no steps defined."
}
}
}
stage("build") {
// wrap logic for this specific stage to keep GitLab informed
gitlabCommitStatus("build") {
// if GitLab action type or target branch is null, Jenkins triggered the job manually outside
// of GitLab or if GitLab push action occurs
if (env.gitlabActionType == null || env.gitlabActionType == "PUSH") {
// perform appropriate action based on target branch
if (env.gitlabTargetBranch == null || env.gitlabTargetBranch == env.GIT_DEV_BRANCH_NAME) {
echo "Building Docker deployment image since manual build or push event occurred..."
sh "docker-compose -f ${DOCKER_COMPOSE_DEV_FILE_NAME_AND_LOCATION} build ${DOCKER_SERVICE_NAME}"
} else if (env.gitlabTargetBranch == env.GIT_MASTER_BRANCH_NAME) {
echo "Building Docker deployment image since push event occurred..."
sh "docker-compose -f ${DOCKER_COMPOSE_PROD_FILE_NAME_AND_LOCATION} build ${DOCKER_SERVICE_NAME}"
} else {
echo "Skipping build since there are no steps defined for the ${env.gitlabBranch} branch."
}
}
// if GitLab merge action occurs
else if (env.gitlabActionType == "MERGE") {
// perform appropriate action based on target branch
if (env.gitlabTargetBranch == env.GIT_DEV_BRANCH_NAME || env.gitlabTargetBranch == env.GIT_MASTER_BRANCH_NAME) {
echo "Building Docker CICD image from merge request..."
sh "docker-compose -f ${DOCKER_COMPOSE_CICD_FILE_NAME_AND_LOCATION} build ${DOCKER_SERVICE_NAME}"
} else {
echo "Skipping build since there are no steps defined for the ${env.gitlabBranch} branch."
}
} else {
echo "Skipping build since the GitLab action has no steps defined."
}
}
}
stage("test") {
// wrap logic for this specific stage to keep GitLab informed
gitlabCommitStatus("test") {
// if GitLab merge action occurs
if (env.gitlabActionType == "MERGE") {
// perform appropriate action based on target branch
if (env.gitlabTargetBranch == env.GIT_DEV_BRANCH_NAME || env.gitlabTargetBranch == env.GIT_MASTER_BRANCH_NAME) {
echo "Running tests within built Docker image..."
sh "docker-compose -f ${DOCKER_COMPOSE_CICD_FILE_NAME_AND_LOCATION} run --rm ${DOCKER_SERVICE_NAME} mvn test"
} else {
echo "Skipping tests since there are no steps defined for the ${env.gitlabBranch} branch."
}
} else {
echo "Skipping tests since the GitLab action has no steps defined."
}
}
}
stage("push") {
// wrap logic for this specific stage to keep GitLab informed
gitlabCommitStatus("push") {
// this allows the Docker Private Registry credentials be used to login, push, or pull images
withCredentials([usernamePassword(credentialsId: env.PRIVATE_DOCKER_REGISTRY_CREDENTIALS_ID,
passwordVariable: 'PRIVATE_DOCKER_REGISTRY_PASSWORD', usernameVariable: 'PRIVATE_DOCKER_REGISTRY_USERNAME')]) {
// if GitLab action type or target branch is null, Jenkins triggered the job manually outside
// of GitLab or if GitLab push action occurs
if (env.gitlabActionType == null || env.gitlabActionType == "PUSH") {
// perform appropriate action based on target branch
if (env.gitlabTargetBranch == null || env.gitlabTargetBranch == env.GIT_DEV_BRANCH_NAME) {
echo "Pushing Maven snapshot artifacts..."
sh "docker-compose -f ${DOCKER_COMPOSE_CICD_FILE_NAME_AND_LOCATION} build ${DOCKER_SERVICE_NAME} > /dev/null 2>&1"
sh "docker-compose -f ${DOCKER_COMPOSE_CICD_FILE_NAME_AND_LOCATION} run --rm ${DOCKER_SERVICE_NAME} mvn deploy -DskipTests"
echo "Pushing built Docker deployment image to private Docker Image Registry..."
sh "docker login --username=${PRIVATE_DOCKER_REGISTRY_USERNAME} --password=${PRIVATE_DOCKER_REGISTRY_PASSWORD} https://${PRIVATE_DOCKER_REGISTRY_FQDN}"
sh "docker-compose -f ${DOCKER_COMPOSE_DEV_FILE_NAME_AND_LOCATION} push ${DOCKER_SERVICE_NAME}"
} else if (env.gitlabTargetBranch == env.GIT_MASTER_BRANCH_NAME) {
echo "Pushing Maven snapshot artifacts..."
sh "docker-compose -f ${DOCKER_COMPOSE_CICD_FILE_NAME_AND_LOCATION} build ${DOCKER_SERVICE_NAME} > /dev/null 2>&1"
sh "docker-compose -f ${DOCKER_COMPOSE_CICD_FILE_NAME_AND_LOCATION} run --rm ${DOCKER_SERVICE_NAME} mvn deploy -DskipTests"
echo "Pushing built Docker deployment image to private Docker Image Registry..."
sh "docker login --username=${PRIVATE_DOCKER_REGISTRY_USERNAME} --password=${PRIVATE_DOCKER_REGISTRY_PASSWORD} https://${PRIVATE_DOCKER_REGISTRY_FQDN}"
sh "docker-compose -f ${DOCKER_COMPOSE_PROD_FILE_NAME_AND_LOCATION} push ${DOCKER_SERVICE_NAME}"
} else {
echo "Skipping push since there are no steps defined for the ${env.gitlabBranch} branch."
}
} else {
echo "Skipping push since the GitLab action has no steps defined."
}
}
}
}
stage("deploy") {
// wrap logic for this specific stage to keep GitLab informed
gitlabCommitStatus("deploy") {
// if GitLab action type or target branch is null, Jenkins triggered the job manually outside
// of GitLab or if GitLab push action occurs
if (env.gitlabActionType == null || env.gitlabActionType == "PUSH") {
// perform appropriate action based on target branch
if (env.gitlabTargetBranch == null || env.gitlabTargetBranch == env.GIT_DEV_BRANCH_NAME) {
echo "Deploying built Docker deployment image to development environment as a service..."
// TODO: insert deployment logic
} else if (env.gitlabTargetBranch == env.GIT_MASTER_BRANCH_NAME) {
echo "Deploying built Docker deployment image to production environment as a service..."
// TODO: insert deployment logic
} else {
echo "Skipping deployment since there are no steps defined for the ${env.gitlabBranch} branch."
}
} else {
echo "Skipping deployment since the GitLab action has no steps defined."
}
}
}
stage("cleanup") {
// wrap logic for this specific stage to keep GitLab informed
gitlabCommitStatus("cleanup") {
// clean up left over artifacts from previous builds
echo "Cleaning up dangling Docker images and volumes..."
sh "docker image prune --force && docker volume prune --force"
}
}
}
}
}
// catch any errors which may occur to allow for proper debugging
catch (Exception exception) {
echo "Something unexpected happened. Please inspect the Jenkins logs."
// notify GitLab that job failed
updateGitlabCommitStatus name: 'build', state: 'failed'
// add comment to GitLab merge request
addGitLabMRComment(comment: "Something unexpected happened. Please inspect the Jenkins logs.")
// re-throw the exception
throw exception as java.lang.Throwable
}
}
// --------------------------------------------------------------------------------------------------------------------------
// </Stages, Steps, Etc.>