-
Notifications
You must be signed in to change notification settings - Fork 148
/
index.js
490 lines (444 loc) · 22.4 KB
/
index.js
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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
// @flow
const Promise = require("bluebird");
const OpenShiftClient = require('openshift-client');
const sleep = require("es7-sleep");
const R = require('ramda');
const { logger } = require('@lagoon/commons/src/local-logging');
const { getOpenShiftInfoForProject, addOrUpdateEnvironment, getEnvironmentByName, addDeployment } = require('@lagoon/commons/src/api');
const { sendToLagoonLogs, initSendToLagoonLogs } = require('@lagoon/commons/src/logs');
const { consumeTasks, initSendToLagoonTasks, createTaskMonitor } = require('@lagoon/commons/src/tasks');
initSendToLagoonLogs();
initSendToLagoonTasks();
const CI = process.env.CI || "false"
const gitSafeBranch = process.env.LAGOON_GIT_SAFE_BRANCH || "master"
const messageConsumer = async msg => {
const {
projectName,
branchName,
sha,
type,
headBranchName,
headSha,
baseBranchName,
baseSha,
pullrequestTitle,
promoteSourceEnvironment,
} = JSON.parse(msg.content.toString())
logger.verbose(`Received DeployOpenshift task for project: ${projectName}, branch: ${branchName}, sha: ${sha}`);
const result = await getOpenShiftInfoForProject(projectName);
const projectOpenShift = result.project
const ocsafety = string => string.toLocaleLowerCase().replace(/[^0-9a-z-]/g,'-')
try {
var safeBranchName = ocsafety(branchName)
var safeProjectName = ocsafety(projectName)
var environmentType = branchName === projectOpenShift.productionEnvironment ? 'production' : 'development';
var gitSha = sha
var projectId = projectOpenShift.id
var openshiftConsole = projectOpenShift.openshift.consoleUrl.replace(/\/$/, "");
var openshiftToken = projectOpenShift.openshift.token || ""
var openshiftProject = projectOpenShift.openshiftProjectPattern ? projectOpenShift.openshiftProjectPattern.replace('${branch}',safeBranchName).replace('${project}', safeProjectName) : `${safeProjectName}-${safeBranchName}`
var openshiftName = projectOpenShift.openshift.name
var openshiftProjectUser = projectOpenShift.openshift.projectUser || ""
var deployPrivateKey = projectOpenShift.customer.privateKey
var gitUrl = projectOpenShift.gitUrl
var subfolder = projectOpenShift.subfolder || ""
var routerPattern = projectOpenShift.openshift.routerPattern ? projectOpenShift.openshift.routerPattern.replace('${branch}',safeBranchName).replace('${project}', safeProjectName) : ""
var prHeadBranchName = headBranchName || ""
var prHeadSha = headSha || ""
var prBaseBranchName = baseBranchName || ""
var prBaseSha = baseSha || ""
var prPullrequestTitle = pullrequestTitle || ""
var graphqlEnvironmentType = environmentType.toUpperCase()
var graphqlGitType = type.toUpperCase()
var openshiftPromoteSourceProject = promoteSourceEnvironment ? `${safeProjectName}-${ocsafety(promoteSourceEnvironment)}` : ""
} catch(error) {
logger.error(`Error while loading information for project ${projectName}`)
logger.error(error)
throw(error)
}
// Deciding which git REF we would like deployed
switch (type) {
case "branch":
// if we have a sha given, we use that, if not we fall back to the branch (which needs be prefixed by `origin/`
var gitRef = gitSha ? gitSha : `origin/${branchName}`
break;
case "pullrequest":
var gitRef = gitSha
break;
case "promote":
var gitRef = `origin/${promoteSourceEnvironment}`
break;
}
// Generates a buildconfig object
const buildconfig = (resourceVersion, secret, type, environment = {}) => {
let buildFromImage = {}
// During CI we want to use the OpenShift Registry for our build Image and use the OpenShift registry for the base Images
if (CI == "true") {
buildFromImage = {
"kind": "ImageStreamTag",
"namespace": "lagoon",
"name": "oc-build-deploy-dind:latest"
}
} else {
// By default we load oc-build-deploy-dind from DockerHub with our current branch as tag
buildFromImage = {
"kind": "DockerImage",
"name": `amazeeiolagoon/${gitSafeBranch}-oc-build-deploy-dind`
}
}
let buildconfig = {
"apiVersion": "v1",
"kind": "BuildConfig",
"metadata": {
"creationTimestamp": null,
"name": "lagoon",
"resourceVersion": resourceVersion
},
"spec": {
"nodeSelector": null,
"postCommit": {},
"resources": {},
"runPolicy": "Serial",
"successfulBuildsHistoryLimit": 1,
"failedBuildsHistoryLimit": 1,
"source": {
"git": {
"uri": gitUrl
},
"type": "Git"
},
"strategy": {
"customStrategy": {
"secrets": [
{
"secretSource": {
"name": secret
},
"mountPath": "/var/run/secrets/lagoon/deployer"
},
{
"secretSource": {
"name": "lagoon-sshkey"
},
"mountPath": "/var/run/secrets/lagoon/ssh"
}
],
"env": [
{
"name": "TYPE",
"value": type
},
{
"name": "GIT_REF",
"value": gitRef
},
{
"name": "SUBFOLDER",
"value": subfolder
},
{
"name": "SAFE_BRANCH",
"value": safeBranchName
},
{
"name": "BRANCH",
"value": branchName
},
{
"name": "SAFE_PROJECT",
"value": safeProjectName
},
{
"name": "PROJECT",
"value": projectName
},
{
"name": "ROUTER_URL",
"value": routerPattern
},
{
"name": "ENVIRONMENT_TYPE",
"value": environmentType
},
{
"name": "OPENSHIFT_NAME",
"value": openshiftName
}
],
"forcePull": true,
"from": buildFromImage,
},
"type": "Custom"
}
}
}
if (CI == "true") {
buildconfig.spec.strategy.customStrategy.env.push({"name": "CI","value": CI})
}
if (type == "pullrequest") {
buildconfig.spec.strategy.customStrategy.env.push({"name": "PR_HEAD_BRANCH","value": prHeadBranchName})
buildconfig.spec.strategy.customStrategy.env.push({"name": "PR_HEAD_SHA","value": prHeadSha})
buildconfig.spec.strategy.customStrategy.env.push({"name": "PR_BASE_BRANCH","value": prBaseBranchName})
buildconfig.spec.strategy.customStrategy.env.push({"name": "PR_BASE_SHA","value": prBaseSha})
buildconfig.spec.strategy.customStrategy.env.push({"name": "PR_TITLE","value": prPullrequestTitle})
}
if (type == "promote") {
buildconfig.spec.strategy.customStrategy.env.push({"name": "PROMOTION_SOURCE_ENVIRONMENT","value": promoteSourceEnvironment})
buildconfig.spec.strategy.customStrategy.env.push({"name": "PROMOTION_SOURCE_OPENSHIFT_PROJECT","value": openshiftPromoteSourceProject})
}
if (!R.isEmpty(projectOpenShift.envVariables)) {
buildconfig.spec.strategy.customStrategy.env.push({"name": "LAGOON_PROJECT_VARIABLES", "value": JSON.stringify(projectOpenShift.envVariables)})
}
if (!R.isEmpty(environment.envVariables)) {
buildconfig.spec.strategy.customStrategy.env.push({"name": "LAGOON_ENVIRONMENT_VARIABLES", "value": JSON.stringify(environment.envVariables)})
}
return buildconfig
}
// OpenShift API object
const openshift = new OpenShiftClient.OApi({
url: openshiftConsole,
insecureSkipTlsVerify: true,
auth: {
bearer: openshiftToken
},
});
// Kubernetes API Object - needed as some API calls are done to the Kubernetes API part of OpenShift and
// the OpenShift API does not support them.
const kubernetes = new OpenShiftClient.Core({
url: openshiftConsole,
insecureSkipTlsVerify: true,
auth: {
bearer: openshiftToken
},
});
// openshift-client does not know about the OpenShift Resources, let's teach it.
openshift.ns.addResource('buildconfigs');
openshift.ns.addResource('rolebindings');
openshift.addResource('projectrequests');
// If we should promote, first check if the source project does exist
if (type == "promote") {
try {
const promotionSourceProjectsGet = Promise.promisify(openshift.projects(openshiftPromoteSourceProject).get, { context: openshift.projects(openshiftPromoteSourceProject) })
await promotionSourceProjectsGet()
logger.info(`${openshiftProject}: Promotion Source Project ${openshiftPromoteSourceProject} exists, continuing`)
} catch (err) {
const error = `${openshiftProject}: Promotion Source Project ${openshiftPromoteSourceProject} does not exists, ${err}`
logger.error(error)
throw new Error(error)
}
}
// Create a new Project if it does not exist
let projectStatus = {}
try {
const projectsGet = Promise.promisify(openshift.projects(openshiftProject).get, { context: openshift.projects(openshiftProject) })
projectStatus = await projectsGet()
logger.info(`${openshiftProject}: Project ${openshiftProject} already exists, continuing`)
} catch (err) {
// a non existing project also throws an error, we check if it's a 404, means it does not exist, so we create it.
if (err.code == 404 || err.code == 403) {
logger.info(`${openshiftProject}: Project ${openshiftProject} does not exist, creating`)
const projectrequestsPost = Promise.promisify(openshift.projectrequests.post, { context: openshift.projectrequests })
await projectrequestsPost({ body: {"apiVersion":"v1","kind":"ProjectRequest","metadata":{"name":openshiftProject},"displayName":`[${projectName}] ${branchName}`} });
} else {
logger.error(err)
throw new Error
}
}
// Update GraphQL API with information about this environment
let environment;
try {
environment = await addOrUpdateEnvironment(branchName, projectId, graphqlGitType, graphqlEnvironmentType, openshiftProject)
logger.info(`${openshiftProject}: Created/Updated Environment in API`)
} catch (err) {
logger.error(err)
throw new Error
}
// Used to create RoleBindings
const rolebindingsPost = Promise.promisify(openshift.ns(openshiftProject).rolebindings.post, { context: openshift.ns(openshiftProject).rolebindings })
// If a project user is given, give it access to our project.
if (openshiftProjectUser) {
try {
const rolebindingsGet = Promise.promisify(openshift.ns(openshiftProject).rolebindings(`${openshiftProjectUser}-edit`).get, { context: openshift.ns(openshiftProject).rolebindings(`${openshiftProjectUser}-edit`) })
projectUserRoleBinding = await rolebindingsGet()
logger.info(`${openshiftProject}: RoleBinding ${openshiftProjectUser}-edit already exists, continuing`)
} catch (err) {
if (err.code == 404) {
logger.info(`${openshiftProject}: RoleBinding ${openshiftProjectUser}-edit does not exists, creating`)
await rolebindingsPost({ body: {"kind":"RoleBinding","apiVersion":"v1","metadata":{"name":`${openshiftProjectUser}-edit`,"namespace":openshiftProject},"roleRef":{"name":"edit"},"subjects":[{"name":openshiftProjectUser,"kind":"User","namespace":openshiftProject}]}})
} else {
logger.error(err)
throw new Error
}
}
}
// Create ServiceAccount if it does not exist yet.
try {
const serviceaccountsGet = Promise.promisify(kubernetes.ns(openshiftProject).serviceaccounts('lagoon-deployer').get, { context: kubernetes.ns(openshiftProject).serviceaccounts('lagoon-deployer') })
projectStatus = await serviceaccountsGet()
logger.info(`${openshiftProject}: ServiceAccount lagoon-deployer already exists, continuing`)
} catch (err) {
if (err.code == 404) {
logger.info(`${openshiftProject}: ServiceAccount lagoon-deployer does not exists, creating`)
const serviceaccountsPost = Promise.promisify(kubernetes.ns(openshiftProject).serviceaccounts.post, { context: kubernetes.ns(openshiftProject).serviceaccounts })
await serviceaccountsPost({ body: {"kind":"ServiceAccount","apiVersion":"v1","metadata":{"name":"lagoon-deployer"} }})
await sleep(2000); // sleep a bit after creating the ServiceAccount for OpenShift to create all the secrets
await rolebindingsPost({ body: {"kind":"RoleBinding","apiVersion":"v1","metadata":{"name":"lagoon-deployer-edit","namespace":openshiftProject},"roleRef":{"name":"edit"},"subjects":[{"name":"lagoon-deployer","kind":"ServiceAccount","namespace":openshiftProject}]}})
} else {
logger.error(err)
throw new Error
}
}
// Give the ServiceAccount access to the Promotion Source Project, it needs two roles: 'view' and 'system:image-puller'
if (type == "promote") {
try {
const promotionSourcRolebindingsGet = Promise.promisify(openshift.ns(openshiftPromoteSourceProject).rolebindings(`${openshiftProject}-lagoon-deployer-view`).get, { context: openshift.ns(openshiftProject).rolebindings(`${openshiftProject}-lagoon-deployer-view`) })
await promotionSourcRolebindingsGet()
logger.info(`${openshiftProject}: RoleBinding ${openshiftProject}-lagoon-deployer-view in ${openshiftPromoteSourceProject} does already exist, continuing`)
} catch (err) {
if (err.code == 404) {
logger.info(`${openshiftProject}: RoleBinding ${openshiftProject}-lagoon-deployer-view in ${openshiftPromoteSourceProject} does not exists, creating`)
const promotionSourceRolebindingsPost = Promise.promisify(openshift.ns(openshiftPromoteSourceProject).rolebindings.post, { context: openshift.ns(openshiftPromoteSourceProject).rolebindings })
await promotionSourceRolebindingsPost({ body: {"kind":"RoleBinding","apiVersion":"v1","metadata":{"name":`${openshiftProject}-lagoon-deployer-view`,"namespace":openshiftPromoteSourceProject},"roleRef":{"name":"view"},"subjects":[{"name":"lagoon-deployer","kind":"ServiceAccount","namespace":openshiftProject}]}})
} else {
logger.error(err)
throw new Error
}
}
try {
const promotionSourceRolebindingsGet = Promise.promisify(openshift.ns(openshiftPromoteSourceProject).rolebindings(`${openshiftProject}-lagoon-deployer-image-puller`).get, { context: openshift.ns(openshiftProject).rolebindings(`${openshiftProject}-lagoon-deployer-image-puller`) })
await promotionSourceRolebindingsGet()
logger.info(`${openshiftProject}: RoleBinding ${openshiftProject}-lagoon-deployer-image-puller in ${openshiftPromoteSourceProject} does already exist, continuing`)
} catch (err) {
if (err.code == 404) {
logger.info(`${openshiftProject}: RoleBinding ${openshiftProject}-lagoon-deployer-image-puller in ${openshiftPromoteSourceProject} does not exists, creating`)
const promotionSourceRolebindingsPost = Promise.promisify(openshift.ns(openshiftPromoteSourceProject).rolebindings.post, { context: openshift.ns(openshiftPromoteSourceProject).rolebindings })
await promotionSourceRolebindingsPost({ body: {"kind":"RoleBinding","apiVersion":"v1","metadata":{"name":`${openshiftProject}-lagoon-deployer-image-puller`,"namespace":openshiftPromoteSourceProject},"roleRef":{"name":"system:image-puller"},"subjects":[{"name":"lagoon-deployer","kind":"ServiceAccount","namespace":openshiftProject}]}})
} else {
logger.error(err)
throw new Error
}
}
}
// Create SSH Key Secret if not exist yet, if it does update it.
let sshKey = {}
const sshKeyBase64 = new Buffer(deployPrivateKey.replace(/\\n/g, "\n")).toString('base64')
try {
const secretsGet = Promise.promisify(kubernetes.ns(openshiftProject).secrets('lagoon-sshkey').get, { context: kubernetes.ns(openshiftProject).secrets('lagoon-sshkey') })
sshKey = await secretsGet()
logger.info(`${openshiftProject}: Secret lagoon-sshkey already exists, updating`)
const secretsPut = Promise.promisify(kubernetes.ns(openshiftProject).secrets('lagoon-sshkey').put, { context: kubernetes.ns(openshiftProject).secrets('lagoon-sshkey') })
await secretsPut({ body: {"apiVersion":"v1","kind":"Secret","metadata":{"name":"lagoon-sshkey", "resourceVersion": sshKey.metadata.resourceVersion },"type":"kubernetes.io/ssh-auth","data":{"ssh-privatekey":sshKeyBase64}}})
} catch (err) {
if (err.code == 404) {
logger.info(`${openshiftProject}: Secret lagoon-sshkey does not exists, creating`)
const secretsPost = Promise.promisify(kubernetes.ns(openshiftProject).secrets.post, { context: kubernetes.ns(openshiftProject).secrets })
await secretsPost({ body: {"apiVersion":"v1","kind":"Secret","metadata":{"name":"lagoon-sshkey"},"type":"kubernetes.io/ssh-auth","data":{"ssh-privatekey":sshKeyBase64}}})
} else {
logger.error(err)
throw new Error
}
}
// Load the Token Secret Name for our created ServiceAccount
const serviceaccountsGet = Promise.promisify(kubernetes.ns(openshiftProject).serviceaccounts("lagoon-deployer").get, { context: kubernetes.ns(openshiftProject).serviceaccounts("lagoon-deployer") })
serviceaccount = await serviceaccountsGet()
// a ServiceAccount can have multiple secrets, we are interested in one that has starts with 'lagoon-deployer-token'
let serviceaccountTokenSecret = '';
for (var key in serviceaccount.secrets) {
if(/^lagoon-deployer-token/.test(serviceaccount.secrets[key].name)) {
serviceaccountTokenSecret = serviceaccount.secrets[key].name
break;
}
}
if (serviceaccountTokenSecret == '') {
throw new Error(`${openshiftProject}: Could not find token secret for ServiceAccount lagoon-deployer`)
}
// Create or update the BuildConfig
try {
const buildConfigsGet = Promise.promisify(openshift.ns(openshiftProject).buildconfigs('lagoon').get, { context: openshift.ns(openshiftProject).buildconfigs('lagoon') })
currentBuildConfig = await buildConfigsGet()
logger.info(`${openshiftProject}: Buildconfig lagoon already exists, updating`)
const buildConfigsPut = Promise.promisify(openshift.ns(openshiftProject).buildconfigs('lagoon').put, { context: openshift.ns(openshiftProject).buildconfigs('lagoon') })
// The OpenShift API needs the current resource Version so it knows that we're updating data of the last known version. This is filled within currentBuildConfig.metadata.resourceVersion
await buildConfigsPut({ body: buildconfig(currentBuildConfig.metadata.resourceVersion, serviceaccountTokenSecret, type, environment.addOrUpdateEnvironment) })
} catch (err) {
// Same as for projects, if BuildConfig does not exist, it throws an error and we check the error is an 404 and with that we know it does not exist.
if (err.code == 404) {
logger.info(`${openshiftProject}: Buildconfig lagoon does not exist, creating`)
const buildConfigsPost = Promise.promisify(openshift.ns(openshiftProject).buildconfigs.post, { context: openshift.ns(openshiftProject).buildconfigs })
// This is a complete new BuildConfig, so the resource version is "0" (it will be updated automatically by OpenShift)
await buildConfigsPost({ body: buildconfig("0", serviceaccountTokenSecret, type, environment.addOrUpdateEnvironment) })
} else {
logger.error(err)
throw new Error
}
}
// Instantiate = Create a new Build from an existing BuildConfig
const buildConfigsInstantiatePost = Promise.promisify(openshift.ns(openshiftProject).buildconfigs('lagoon/instantiate').post, { context: openshift.ns(openshiftProject).buildconfigs('lagoon/instantiate') })
const build = await buildConfigsInstantiatePost({body: {"kind":"BuildRequest","apiVersion":"v1","metadata":{"name":"lagoon"}}})
const buildName = build.metadata.name
try {
const convertDateFormat = R.init;
const apiEnvironment = await getEnvironmentByName(branchName, projectId);
await addDeployment(buildName, build.status.phase.toUpperCase(), convertDateFormat(build.metadata.creationTimestamp), apiEnvironment.environmentByName.id, build.metadata.uid);
} catch (error) {
logger.error(`Could not save deployment for project ${projectId}, build ${buildName}. Message: ${error}`);
}
logger.verbose(`${openshiftProject}: Running build: ${buildName}`)
const monitorPayload = {
buildName: buildName,
projectName: projectName,
openshiftProject: openshiftProject,
branchName: branchName,
sha: sha
}
const taskMonitorLogs = await createTaskMonitor('builddeploy-openshift', monitorPayload)
let logMessage = ''
if (gitSha) {
logMessage = `\`${branchName}\` (${buildName})`
} else {
logMessage = `\`${branchName}\``
}
sendToLagoonLogs('start', projectName, "", "task:builddeploy-openshift:start", {},
`*[${projectName}]* ${logMessage}`
)
}
const deathHandler = async (msg, lastError) => {
const {
projectName,
branchName,
sha
} = JSON.parse(msg.content.toString())
let logMessage = ''
if (sha) {
logMessage = `\`${branchName}\` (${sha.substring(0, 7)})`
} else {
logMessage = `\`${branchName}\``
}
sendToLagoonLogs('error', projectName, "", "task:builddeploy-openshift:error", {},
`*[${projectName}]* ${logMessage} ERROR:
\`\`\`
${lastError}
\`\`\``
)
}
const retryHandler = async (msg, error, retryCount, retryExpirationSecs) => {
const {
projectName,
branchName,
sha
} = JSON.parse(msg.content.toString())
let logMessage = ''
if (sha) {
logMessage = `\`${branchName}\` (${sha.substring(0, 7)})`
} else {
logMessage = `\`${branchName}\``
}
sendToLagoonLogs('warn', projectName, "", "task:builddeploy-openshift:retry", {error: error.message, msg: JSON.parse(msg.content.toString()), retryCount: retryCount},
`*[${projectName}]* ${logMessage} ERROR:
\`\`\`
${error}
\`\`\`
Retrying deployment in ${retryExpirationSecs} secs`
)
}
consumeTasks('builddeploy-openshift', messageConsumer, retryHandler, deathHandler)