UNPKG

serverless-python-requirements

Version:
277 lines (257 loc) 8.55 kB
/* jshint ignore:start */ 'use strict'; const BbPromise = require('bluebird'); const fse = require('fs-extra'); const values = require('lodash.values'); const { addVendorHelper, removeVendorHelper, packRequirements, } = require('./lib/zip'); const { injectAllRequirements } = require('./lib/inject'); const { layerRequirements } = require('./lib/layer'); const { installAllRequirements } = require('./lib/pip'); const { pipfileToRequirements } = require('./lib/pipenv'); const { cleanup, cleanupCache } = require('./lib/clean'); BbPromise.promisifyAll(fse); /** * Plugin for Serverless 1.x that bundles python requirements! */ class ServerlessPythonRequirements { /** * get the custom.pythonRequirements contents, with defaults set * @return {Object} */ get options() { const options = Object.assign( { slim: false, slimPatterns: false, slimPatternsAppendDefaults: true, zip: false, layer: false, cleanupZipHelper: true, invalidateCaches: false, fileName: 'requirements.txt', usePipenv: true, usePoetry: true, pythonBin: process.platform === 'win32' ? 'python.exe' : this.serverless.service.provider.runtime || 'python', dockerizePip: false, dockerSsh: false, dockerPrivateKey: null, dockerImage: null, dockerFile: null, dockerEnv: false, dockerBuildCmdExtraArgs: [], dockerRunCmdExtraArgs: [], dockerExtraFiles: [], dockerRootless: false, useStaticCache: true, useDownloadCache: true, cacheLocation: false, staticCacheMaxVersions: 0, pipCmdExtraArgs: [], noDeploy: [], vendor: '', requirePoetryLockFile: false, poetryWithGroups: [], poetryWithoutGroups: [], poetryOnlyGroups: [], }, (this.serverless.service.custom && this.serverless.service.custom.pythonRequirements) || {} ); if ( options.pythonBin === this.serverless.service.provider.runtime && !options.pythonBin.startsWith('python') ) { options.pythonBin = 'python'; } if (/python3[0-9]+/.test(options.pythonBin)) { // "google" and "scaleway" providers' runtimes use python3XX options.pythonBin = options.pythonBin.replace(/3([0-9]+)/, '3.$1'); } if (options.dockerizePip === 'non-linux') { options.dockerizePip = process.platform !== 'linux'; } if (options.dockerizePip && process.platform === 'win32') { options.pythonBin = 'python'; } if ( !options.dockerizePip && (options.dockerSsh || options.dockerImage || options.dockerFile || options.dockerPrivateKey) ) { if (!this.warningLogged) { if (this.log) { this.log.warning( 'You provided a docker related option but dockerizePip is set to false.' ); } else { this.serverless.cli.log( 'WARNING: You provided a docker related option but dockerizePip is set to false.' ); } this.warningLogged = true; } } if (options.dockerImage && options.dockerFile) { throw new Error( 'Python Requirements: you can provide a dockerImage or a dockerFile option, not both.' ); } if (options.layer) { // If layer was set as a boolean, set it to an empty object to use the layer defaults. if (options.layer === true) { options.layer = {}; } } return options; } get targetFuncs() { let inputOpt = this.serverless.processedInput.options; return inputOpt.function ? [this.serverless.service.functions[inputOpt.function]] : values(this.serverless.service.functions).filter((func) => !func.image); } /** * The plugin constructor * @param {Object} serverless * @param {Object} options * @param {Object} v3Utils * @return {undefined} */ constructor(serverless, cliOptions, v3Utils) { this.serverless = serverless; this.servicePath = this.serverless.config.servicePath; this.warningLogged = false; if ( this.serverless.configSchemaHandler && this.serverless.configSchemaHandler.defineFunctionProperties ) { this.serverless.configSchemaHandler.defineFunctionProperties('aws', { properties: { module: { type: 'string', }, }, }); } if (v3Utils) { this.log = v3Utils.log; this.progress = v3Utils.progress; this.writeText = v3Utils.writeText; } this.commands = { requirements: { commands: { clean: { usage: 'Remove .requirements and requirements.zip', lifecycleEvents: ['clean'], }, install: { usage: 'install requirements manually', lifecycleEvents: ['install'], }, cleanCache: { usage: 'Removes all items in the pip download/static cache (if present)', lifecycleEvents: ['cleanCache'], }, }, }, }; if (this.serverless.cli.generateCommandsHelp) { Object.assign(this.commands.requirements, { usage: 'Serverless plugin to bundle Python packages', lifecycleEvents: ['requirements'], }); } else { this.commands.requirements.type = 'container'; } this.dockerImageForFunction = (funcOptions) => { const runtime = funcOptions.runtime || this.serverless.service.provider.runtime; const architecture = funcOptions.architecture || this.serverless.service.provider.architecture || 'x86_64'; const defaultImage = `public.ecr.aws/sam/build-${runtime}:latest-${architecture}`; return this.options.dockerImage || defaultImage; }; const isFunctionRuntimePython = (args) => { // If functionObj.runtime is undefined, python. if (!args[1].functionObj || !args[1].functionObj.runtime) { return true; } return args[1].functionObj.runtime.startsWith('python'); }; const clean = () => BbPromise.bind(this).then(cleanup).then(removeVendorHelper); const setupArtifactPathCapturing = () => { // Reference: // https://github.com/serverless/serverless/blob/9591d5a232c641155613d23b0f88ca05ea51b436/lib/plugins/package/lib/packageService.js#L139 // The packageService#packageFunction does set artifact path back to the function config. // As long as the function config's "package" attribute wasn't undefined, we can still use it // later to access the artifact path. for (const functionName in this.serverless.service.functions) { if (!serverless.service.functions[functionName].package) { serverless.service.functions[functionName].package = {}; } } }; const before = () => { if (!isFunctionRuntimePython(arguments)) { return; } return BbPromise.bind(this) .then(pipfileToRequirements) .then(addVendorHelper) .then(installAllRequirements) .then(packRequirements) .then(setupArtifactPathCapturing); }; const after = () => { if (!isFunctionRuntimePython(arguments)) { return; } return BbPromise.bind(this) .then(removeVendorHelper) .then(layerRequirements) .then(() => injectAllRequirements.bind(this)( arguments[1].functionObj && arguments[1].functionObj.package.artifact ) ); }; const invalidateCaches = () => { if (this.options.invalidateCaches) { return clean; } return BbPromise.resolve(); }; const cleanCache = () => BbPromise.bind(this).then(cleanupCache); this.hooks = { 'after:package:cleanup': invalidateCaches, 'before:package:createDeploymentArtifacts': before, 'after:package:createDeploymentArtifacts': after, 'before:deploy:function:packageFunction': before, 'after:deploy:function:packageFunction': after, 'requirements:requirements': () => { this.serverless.cli.generateCommandsHelp(['requirements']); return BbPromise.resolve(); }, 'requirements:install:install': before, 'requirements:clean:clean': clean, 'requirements:cleanCache:cleanCache': cleanCache, }; } } module.exports = ServerlessPythonRequirements;