UNPKG

screwdriver-api

Version:

API server for the Screwdriver.cd service

276 lines (225 loc) • 10.3 kB
'use strict'; const urlLib = require('url'); const boom = require('@hapi/boom'); const validationSchema = require('screwdriver-data-schema'); const ANNOT_RESTRICT_PR = 'screwdriver.cd/restrictPR'; const { getScmUri, isStageTeardown } = require('../helper'); const { createEvent } = require('./helper/createEvent'); module.exports = () => ({ method: 'POST', path: '/events', options: { description: 'Create and start a event', notes: 'Create and start a specific event', tags: ['api', 'events'], auth: { strategies: ['token'], scope: ['user', '!guest', 'pipeline'] }, handler: async (request, h) => { const { buildFactory, jobFactory, eventFactory, pipelineFactory, userFactory } = request.server.app; const { buildId, causeMessage, creator } = request.payload; const { scmContext, username, scope } = request.auth.credentials; const { scm } = eventFactory; const { isValidToken } = request.server.plugins.pipelines; const { updateAdmins } = request.server.plugins.events; let { pipelineId, startFrom, parentBuildId, parentBuilds, groupEventId, parentEventId, prNum } = request.payload; // Validation: Prevent event creation if startFrom is a stage teardown and parentEventID does not exist (start case) if (isStageTeardown(startFrom) && !parentEventId) { throw boom.badRequest('Event cannot be started from a stage teardown'); } // restart case if (buildId) { const b = await buildFactory.get(buildId); const j = await jobFactory.get(b.jobId); ({ pipelineId, name: startFrom } = j); ({ parentBuildId, eventId: parentEventId } = b); if (b.parentBuilds) { parentBuilds = b.parentBuilds; } if (b.eventId && !groupEventId) { const parentEvent = await eventFactory.get(b.eventId); groupEventId = parentEvent.groupEventId || b.eventId; } } const payload = { pipelineId, scmContext, startFrom, type: 'pipeline', username, meta: request.payload.meta // always exists because default is {} }; if (parentEventId) { payload.parentEventId = parentEventId; } if (parentBuildId) { payload.parentBuildId = parentBuildId; } if (groupEventId) { payload.groupEventId = groupEventId; } if (parentBuilds) { payload.parentBuilds = parentBuilds; } if (causeMessage) { payload.causeMessage = causeMessage; } if (creator) { payload.creator = creator; if (creator.username !== 'sd:scheduler') { payload.creator.username = username; } } else if (scope.includes('pipeline')) { payload.creator = { name: 'Pipeline Access Token', // Display name username }; } // Check for startFrom if (!startFrom) { throw boom.badRequest('Missing "startFrom" field'); } // Trigger "~pr" needs to have PR number given // Note: To kick start builds for all jobs under a PR, // you need both the prNum and the trigger "~pr" as startFrom if (startFrom.match(validationSchema.config.regex.PR_TRIGGER) && !prNum) { throw boom.badRequest('Trigger "~pr" must be accompanied by a PR number'); } if (!prNum) { // If PR number isn't given, induce it from "startFrom" // Match PR-prNum, then extract prNum // e.g. if startFrom is "PR-1:main", prNumFullName will be "PR-1"; prNum will be "1" const prNumFullName = startFrom.match(validationSchema.config.regex.PR_JOB_NAME); prNum = prNumFullName ? prNumFullName[1].split('-')[1] : null; } // Fetch the pipeline and user models const [pipeline, user] = await Promise.all([ pipelineFactory.get(pipelineId), userFactory.get({ username, scmContext }) ]); if (!pipeline) { throw boom.notFound(); } else if (pipeline.state === 'INACTIVE') { throw boom.badRequest('Cannot create an event for an inactive pipeline'); } payload.scmContext = pipeline.scmContext; // In pipeline scope, check if the token is allowed to the pipeline if (!isValidToken(pipeline.id, request.auth.credentials)) { throw boom.unauthorized('Token does not have permission to this pipeline'); } // Use parent's scmUri if pipeline is child pipeline and using read-only SCM const scmUri = await getScmUri({ pipeline, pipelineFactory }); // Check the user's permission let permissions; try { permissions = await user.getPermissions(scmUri, pipeline.scmContext, pipeline.scmRepo); } catch (err) { if (err.statusCode === 403 && pipeline.scmRepo && pipeline.scmRepo.private) { throw boom.notFound(); } throw boom.boomify(err, { statusCode: err.statusCode }); } // Update admins if (!prNum) { await updateAdmins({ permissions, pipeline, username }); } // Get scmConfig const token = await user.unsealToken(); const scmConfig = { prNum, scmContext: pipeline.scmContext, scmUri: pipeline.scmUri, scmRepo: pipeline.scmRepo, token }; // Get and set PR data; update admins if (prNum) { payload.prNum = String(prNum); payload.type = 'pr'; const [files, prInfo] = await Promise.all([ scm.getChangedFiles({ webhookConfig: null, type: 'pr', ...scmConfig }), scm.getPrInfo(scmConfig) ]).catch(err => { throw boom.boomify(err, { statusCode: err.statusCode }); }); if (files && files.length) { payload.changedFiles = files; } payload.prInfo = prInfo; payload.prRef = prInfo.ref; payload.prSource = prInfo.prSource; payload.chainPR = pipeline.chainPR; let restrictPR = 'none'; if (pipeline.annotations && pipeline.annotations[ANNOT_RESTRICT_PR]) { restrictPR = pipeline.annotations[ANNOT_RESTRICT_PR]; } // PR author should be able to rerun their own PR build if restrictPR is not on if (restrictPR !== 'none' || prInfo.username !== username) { // Remove user from admins await updateAdmins({ permissions, pipeline, username }); } } let sha; try { // User has good permissions, create an event sha = await scm.getCommitSha(scmConfig); } catch (err) { if (err.statusCode) { throw boom.boomify(err, { statusCode: err.statusCode }); } } payload.sha = sha; // If there is parentEvent, pass workflowGraph, meta and sha to payload // Skip PR, for PR builds, we should always start from latest commit if (payload.parentEventId) { const parentEvent = await eventFactory.get(parentEventId); let mergedParameters = payload.meta.parameters || {}; payload.baseBranch = parentEvent.baseBranch || null; // Merge parameters if they exist in the parent event and not in the payload if (!payload.meta.parameters && parentEvent.meta && parentEvent.meta.parameters) { mergedParameters = parentEvent.meta.parameters; } delete payload.meta.parameters; // Copy meta from parent event if payload.meta is empty except for the parameters if (Object.keys(payload.meta).length === 0) { payload.meta = { ...parentEvent.meta }; } if (Object.keys(mergedParameters).length > 0) { payload.meta.parameters = mergedParameters; } if (!prNum) { payload.workflowGraph = parentEvent.workflowGraph; payload.sha = parentEvent.sha; if (parentEvent.configPipelineSha) { payload.configPipelineSha = parentEvent.configPipelineSha; } } } const event = await createEvent(payload, request.server); if (event.builds === null) { return boom.notFound('No jobs to start.'); } // everything succeeded, inform the user const location = urlLib.format({ host: request.headers.host, port: request.headers.port, protocol: request.server.info.protocol, pathname: `${request.path}/${event.id}` }); return h.response(event.toJson()).header('Location', location).code(201); }, validate: { payload: validationSchema.models.event.create } } });