UNPKG

screwdriver-api

Version:

API server for the Screwdriver.cd service

201 lines (175 loc) • 7.74 kB
/* eslint no-underscore-dangle: ["error", { "allow": ["_data", "_shot"] }] */ 'use strict'; const urlLib = require('url'); const boom = require('@hapi/boom'); const schema = require('screwdriver-data-schema'); const validator = require('screwdriver-command-validator'); const hoek = require('@hapi/hoek'); const req = require('screwdriver-request'); const VERSION_REGEX = schema.config.regex.VERSION; const DEFAULT_BYTES = 1024 * 1024 * 1024; // 1GB /** * Publish file to the store * @method publishFileToStore * @param {CommandFactory} commandFactory commandFactory * @param {Object} config Command config * @param {Uint8Array} file File published to the store * @param {String} storeUrl URL to the store * @param {String} authToken Bearer Token to be passed to the store * @return {Promise} */ function publishFileToStore(commandFactory, config, file, storeUrl, authToken) { const [, major, minor] = VERSION_REGEX.exec(config.version); const searchVersion = minor ? `${major}${minor}` : major; let publishVersion; return commandFactory .getCommand(`${config.namespace}/${config.name}@${searchVersion}`) .then(latest => { if (!latest) { publishVersion = minor ? `${major}${minor}.0` : `${major}.0.0`; } else { // eslint-disable-next-line max-len const [, latestMajor, latestMinor, latestPatch] = VERSION_REGEX.exec(latest.version); const patch = parseInt(latestPatch.slice(1), 10) + 1; publishVersion = `${latestMajor}${latestMinor}.${patch}`; } return publishVersion; }) .then(version => { const options = { url: `${storeUrl}/v1/commands/${config.namespace}/${config.name}/${version}`, method: 'POST', headers: { Authorization: authToken, 'Content-Type': 'application/octet-stream' }, body: file }; return req(options); }) .then(response => { if (response.statusCode !== 202) { throw new Error(`An error occurred when posting file to the store:${response.body.message}`); } return commandFactory.create(config); }); } /** * Check multipart payload * @method checkValidMultipartPayload * @param {Object} data payload data * @return {Object} */ function checkValidMultipartPayload(data) { const result = { valid: true, message: '' }; if (data.spec === undefined) { result.valid = false; result.message = 'Posted with multipart that has no spec.'; return result; } const commandSpec = JSON.parse(data.spec); const commandBin = data.file; if (commandBin === undefined) { result.valid = false; result.message = 'Posted with multipart that has no binary.'; if (commandSpec.format === 'binary') { result.message = 'Binary command should post with a binary file'; } else if (commandSpec.format === 'habitat' && commandSpec.habitat.mode === 'local') { result.message = 'Habitat local mode should post with a binary file'; } return result; } return result; } module.exports = () => ({ method: 'POST', path: '/commands', options: { description: 'Create a new command', notes: 'Create a specific command', tags: ['api', 'commands'], auth: { strategies: ['token'], scope: ['build', '!guest'] }, payload: { parse: true, maxBytes: DEFAULT_BYTES, allow: ['multipart/form-data', 'application/json'], multipart: true }, handler: async (request, h) => { const data = request.payload; const { isPR } = request.auth.credentials; let commandSpec; let commandBin; let multipartCheckResult = { valid: false }; // if Content-type is multipart/form-data, both command file and meta are posted if (request.headers['content-type'].startsWith('multipart/form-data')) { multipartCheckResult = checkValidMultipartPayload(data); if (multipartCheckResult.valid) { commandSpec = data.spec; commandBin = data.file; } else { return boom.badRequest(multipartCheckResult.message); } } else { commandSpec = data.yaml; } return validator(commandSpec) .then(config => { if (config.errors.length > 0) { throw boom.badRequest( `Command has invalid format: ${config.errors.length} error(s).`, config.errors ); } const { commandFactory } = request.server.app; const { pipelineFactory } = request.server.app; const { pipelineId } = request.auth.credentials; return Promise.all([ pipelineFactory.get(pipelineId), commandFactory.list({ params: { namespace: config.command.namespace, name: config.command.name } }) ]).then(([pipeline, commands]) => { const commandConfig = hoek.applyToDefaults(config.command, { pipelineId: pipeline.id }); // If command name exists, but this build's pipelineId is not the same as command's pipelineId // Then this build does not have permission to publish if (isPR || (commands.length !== 0 && pipeline.id !== commands[0].pipelineId)) { throw boom.forbidden('Not allowed to publish this command'); } // If command name doesn't exist yet, or exists and has good permission, then create // Create would automatically bump the patch version // If command format is binary or habitat local mode, binary file also has to be posted to the store return !multipartCheckResult.valid ? commandFactory.create(commandConfig) : publishFileToStore( commandFactory, commandConfig, commandBin, request.server.app.ecosystem.store, request.headers.authorization ); }); }) .then(command => { const location = urlLib.format({ host: request.headers.host, port: request.headers.port, protocol: request.server.info.protocol, pathname: `${request.path}/${command.id}` }); return h.response(command.toJson()).header('Location', location).code(201); }) .catch(err => { throw err; }); } } });