UNPKG

serverless-wsgi

Version:
885 lines (778 loc) 26.2 kB
"use strict"; const BbPromise = require("bluebird"); const _ = require("lodash"); const path = require("path"); const fse = BbPromise.promisifyAll(require("fs-extra")); const child_process = require("child_process"); const commandExists = require("command-exists"); const overrideStdoutWrite = require("process-utils/override-stdout-write"); class ServerlessWSGI { validate() { return new BbPromise((resolve) => { let handlersFixed = false; _.each(this.serverless.service.functions, (func) => { if (func.handler == "wsgi.handler") { func.handler = func.handler.replace( "wsgi.handler", "wsgi_handler.handler" ); handlersFixed = true; } }); if (handlersFixed) { this.serverless.cli.log( 'Warning: Please change "wsgi.handler" to "wsgi_handler.handler" in serverless.yml' ); this.serverless.cli.log( 'Warning: Using "wsgi.handler" still works but has been deprecated and will be removed' ); this.serverless.cli.log( "Warning: More information at https://github.com/logandk/serverless-wsgi/issues/84" ); } this.enableRequirements = !_.includes( this.serverless.service.plugins, "serverless-python-requirements" ); this.pipArgs = null; this.appPath = this.serverless.config.servicePath; if ( this.serverless.service.custom && this.serverless.service.custom.wsgi ) { if (this.serverless.service.custom.wsgi.app) { this.wsgiApp = this.serverless.service.custom.wsgi.app; this.appPath = path.dirname(path.join(this.appPath, this.wsgiApp)); } if (_.isBoolean(this.serverless.service.custom.wsgi.packRequirements)) { this.enableRequirements = this.serverless.service.custom.wsgi.packRequirements; } this.pipArgs = this.serverless.service.custom.wsgi.pipArgs; } if (this.enableRequirements) { this.requirementsInstallPath = path.join(this.appPath, ".requirements"); } this.packageRootPath = this.serverless.config.servicePath; if ( this.serverless.service.package && this.serverless.service.package.individually && this.serverless.config.servicePath != this.appPath ) { let handler = _.find(this.serverless.service.functions, (fun) => _.includes(fun.handler, "wsgi_handler.handler") ); // serverless-python-requirements supports packaging individual functions // by specifying a Python module, in which case the handler needs to be installed // in the module root, rather than the service root if (handler && handler.module) { this.packageRootPath = this.appPath; this.wsgiApp = path.basename(this.wsgiApp); } } resolve(); }); } configurePackaging() { return new BbPromise((resolve) => { this.serverless.service.package = this.serverless.service.package || {}; this.serverless.service.package.patterns = this.serverless.service.package.patterns || []; this.serverless.service.package.patterns = _.union( this.serverless.service.package.patterns, _.map( ["wsgi_handler.py", "serverless_wsgi.py", ".serverless-wsgi"], (artifact) => path.join( path.relative( this.serverless.config.servicePath, this.packageRootPath ), artifact ) ) ); if (this.enableRequirements) { this.serverless.service.package.patterns.push( `!${path.join( path.relative(this.serverless.config.servicePath, this.appPath), ".requirements/**" )}` ); } resolve(); }); } locatePython() { return new BbPromise((resolve) => { if ( this.serverless.service.custom && this.serverless.service.custom.wsgi && this.serverless.service.custom.wsgi.pythonBin ) { this.serverless.cli.log( `Using Python specified in "pythonBin": ${this.serverless.service.custom.wsgi.pythonBin}` ); this.pythonBin = this.serverless.service.custom.wsgi.pythonBin; return resolve(); } if (this.serverless.service.provider.runtime) { if (commandExists.sync(this.serverless.service.provider.runtime)) { this.serverless.cli.log( `Using Python specified in "runtime": ${this.serverless.service.provider.runtime}` ); this.pythonBin = this.serverless.service.provider.runtime; return resolve(); } else { this.serverless.cli.log( `Python executable not found for "runtime": ${this.serverless.service.provider.runtime}` ); } } this.serverless.cli.log("Using default Python executable: python"); this.pythonBin = "python"; resolve(); }); } getWsgiHandlerConfiguration() { const config = { app: this.wsgiApp }; if (_.isArray(this.serverless.service.custom.wsgi.textMimeTypes)) { config.text_mime_types = this.serverless.service.custom.wsgi.textMimeTypes; } return config; } packWsgiHandler(verbose = true) { if (!this.wsgiApp) { this.serverless.cli.log( "Warning: No WSGI app specified, omitting WSGI handler from package" ); return BbPromise.resolve(); } if (verbose) { this.serverless.cli.log("Packaging Python WSGI handler..."); } return BbPromise.all([ fse.copyAsync( path.resolve(__dirname, "wsgi_handler.py"), path.join(this.packageRootPath, "wsgi_handler.py") ), fse.copyAsync( path.resolve(__dirname, "serverless_wsgi.py"), path.join(this.packageRootPath, "serverless_wsgi.py") ), fse.writeFileAsync( path.join(this.packageRootPath, ".serverless-wsgi"), JSON.stringify(this.getWsgiHandlerConfiguration()) ), ]); } packRequirements() { return new BbPromise((resolve, reject) => { if (!this.enableRequirements) { return resolve(); } let args = [path.resolve(__dirname, "requirements.py")]; if (this.pipArgs) { args.push("--pip-args"); args.push(this.pipArgs); } if (this.wsgiApp) { args.push(path.resolve(__dirname, "requirements.txt")); } const requirementsFile = path.join(this.appPath, "requirements.txt"); if (fse.existsSync(requirementsFile)) { args.push(requirementsFile); } else { if (!this.wsgiApp) { return resolve(); } } args.push(this.requirementsInstallPath); this.serverless.cli.log("Packaging required Python packages..."); const res = child_process.spawnSync(this.pythonBin, args, { encoding: "utf8", }); if (res.error) { if (res.error.code == "ENOENT") { return reject( `Unable to run Python executable: ${this.pythonBin}. Use the "pythonBin" option to set your Python executable explicitly.` ); } else { return reject(res.error); } } if (res.status != 0) { return reject(res.stderr); } resolve(); }); } linkRequirements() { return new BbPromise((resolve, reject) => { if (!this.enableRequirements) { return resolve(); } if (fse.existsSync(this.requirementsInstallPath)) { this.serverless.cli.log("Linking required Python packages..."); fse.readdirSync(this.requirementsInstallPath).map((file) => { let relativePath = path.join( path.relative(this.serverless.config.servicePath, this.appPath), file ); this.serverless.service.package.patterns.push(relativePath); this.serverless.service.package.patterns.push(`${relativePath}/**`); try { fse.symlinkSync(`${this.requirementsInstallPath}/${file}`, file); } catch (exception) { let linkConflict = false; try { linkConflict = fse.readlinkSync(file) !== `${this.requirementsInstallPath}/${file}`; } catch (e) { linkConflict = true; } if (linkConflict) { return reject( `Unable to link dependency '${file}' ` + "because a file by the same name exists in this service" ); } } }); } resolve(); }); } checkWerkzeugPresent() { return new BbPromise((resolve) => { if (!this.wsgiApp || !this.enableRequirements) { return resolve(); } const hasWerkzeug = _.includes(fse.readdirSync(this.appPath), "werkzeug"); if (!hasWerkzeug) { this.serverless.cli.log( "Warning: Could not find werkzeug, please add it to your requirements.txt" ); } resolve(); }); } unlinkRequirements() { return new BbPromise((resolve) => { if (!this.enableRequirements) { return resolve(); } if (fse.existsSync(this.requirementsInstallPath)) { this.serverless.cli.log("Unlinking required Python packages..."); fse.readdirSync(this.requirementsInstallPath).map((file) => { if (fse.existsSync(file)) { fse.unlinkSync(file); } }); } resolve(); }); } cleanRequirements() { if (!this.enableRequirements) { return BbPromise.resolve(); } return fse.removeAsync(this.requirementsInstallPath); } cleanup() { const artifacts = [ "wsgi_handler.py", "serverless_wsgi.py", ".serverless-wsgi", ]; return BbPromise.all( _.map(artifacts, (artifact) => fse.removeAsync(path.join(this.packageRootPath, artifact)) ) ); } loadEnvVars() { return new BbPromise((resolve) => { const providerEnvVars = _.omitBy( this.serverless.service.provider.environment || {}, _.isObject ); _.merge(process.env, providerEnvVars); _.each(this.serverless.service.functions, (func) => { if (_.includes(func.handler, "wsgi_handler.handler")) { const functionEnvVars = _.omitBy(func.environment || {}, _.isObject); _.merge(process.env, functionEnvVars); } }); resolve(); }); } serve() { return new BbPromise((resolve, reject) => { if (!this.wsgiApp) { return reject( 'Missing WSGI app, please specify custom.wsgi.app. For instance, if you have a Flask application "app" in "api.py", set the Serverless custom.wsgi.app configuration option to: api.app' ); } const port = this.options.port || 5000; const host = this.options.host || "localhost"; const disable_threading = this.options["disable-threading"] || false; const num_processes = this.options["num-processes"] || 1; const ssl = this.options.ssl || false; const ssl_pub = this.options["ssl-pub"] || ""; const ssl_pri = this.options["ssl-pri"] || ""; var args = [ path.resolve(__dirname, "serve.py"), this.packageRootPath, this.wsgiApp, port, host, ]; if (num_processes > 1) { args.push("--num-processes", num_processes); } if (disable_threading) { args.push("--disable-threading"); } if (ssl) { args.push("--ssl"); } if (ssl_pub) { args.push("--ssl-pub", ssl_pub); } if (ssl_pri) { args.push("--ssl-pri", ssl_pri); } var status = child_process.spawnSync(this.pythonBin, args, { stdio: "inherit", }); if (status.error) { if (status.error.code == "ENOENT") { reject( `Unable to run Python executable: ${this.pythonBin}. Use the "pythonBin" option to set your Python executable explicitly.` ); } else { reject(status.error); } } else { resolve(); } }); } findHandler() { const functionName = this.options.function || this.options.f; if (functionName) { // If the function name is specified, return it directly if (this.serverless.service.functions[functionName]) { return functionName; } else { throw new Error(`Function "${functionName}" not found.`); } } else { return _.findKey(this.serverless.service.functions, (fun) => _.includes(fun.handler, "wsgi_handler.handler") ); } } invokeHandler(command, data, local) { let handlerFunction; try { handlerFunction = this.findHandler(); } catch (error) { return BbPromise.reject(error.message); } if (!handlerFunction) { return BbPromise.reject( "No functions were found with handler: wsgi_handler.handler" ); } // We're going to call the provider-agnostic invoke plugin, which has // no proper plugin-facing API. Instead, the current CLI options are modified // to match those of an invoke call. this.serverless.pluginManager.cliOptions.function = handlerFunction; this.options.function = handlerFunction; this.options.data = JSON.stringify({ "_serverless-wsgi": { command: command, data: data, }, }); this.serverless.pluginManager.cliOptions.data = JSON.stringify({ "_serverless-wsgi": { command: command, data: data, }, }); this.serverless.pluginManager.cliOptions.context = undefined; this.serverless.pluginManager.cliOptions.f = this.serverless.pluginManager.cliOptions.function; this.serverless.pluginManager.cliOptions.d = this.serverless.pluginManager.cliOptions.data; this.serverless.pluginManager.cliOptions.c = this.serverless.pluginManager.cliOptions.context; this.options.context = undefined; this.options.f = this.options.function; this.options.d = this.options.data; this.options.c = this.options.context; // The invoke plugin prints the response to the console as JSON. When invoking commands // remotely, we get a string back and we want it to appear in the console as it would have // if it was invoked locally. // // We capture stdout output in order to parse the array returned from the lambda invocation, // then restore stdout. let output = ""; /* eslint-disable no-unused-vars */ const { originalStdoutWrite, // Original `write` bound to `process.stdout`#noqa originalWrite, // Original `write` on its own restoreStdoutWrite, // Allows to restore previous state } = overrideStdoutWrite( // process.stdout.write replacement ( orig, // data input originalStdoutWrite // // Original `write` bound to `process.stdout` ) => { output += orig; } ); /* eslint-enable no-unused-vars */ return this.serverless.pluginManager .run(local ? ["invoke", "local"] : ["invoke"]) .then( () => new BbPromise((resolve, reject) => { output = _.trimEnd(output, "\n"); try { output = JSON.parse(output); } catch (e) { // Swallow exception } if (_.isArray(output) && output.length == 2) { const return_code = output[0]; const output_data = _.isString(output[1]) ? _.trimEnd(output[1], "\n") : output[1]; if (return_code == 0) { this.serverless.cli.log(output_data); } else { return reject(new this.serverless.classes.Error(output_data)); } } else { this.serverless.cli.log(output); } return resolve(); }) ) .finally(() => { restoreStdoutWrite(); }); /* eslint-enable no-console */ } command(local) { let data = null; if (this.options.command) { data = this.options.command; } else if (this.options.file) { data = fse.readFileSync(this.options.file, "utf8"); } else { return BbPromise.reject( "Please provide either a command (-c) or a file (-f)" ); } return this.invokeHandler("command", data, local); } exec(local) { let data = null; if (this.options.command) { data = this.options.command; } else if (this.options.file) { data = fse.readFileSync(this.options.file, "utf8"); } else { return BbPromise.reject( "Please provide either a command (-c) or a file (-f)" ); } return this.invokeHandler("exec", data, local); } manage(local) { return this.invokeHandler("manage", this.options.command, local); } flask(local) { return this.invokeHandler("flask", this.options.command, local); } constructor(serverless, options) { this.serverless = serverless; this.options = options; this.commands = { wsgi: { usage: "Deploy Python WSGI applications", lifecycleEvents: ["wsgi"], commands: { serve: { usage: "Serve the WSGI application locally", lifecycleEvents: ["serve"], options: { port: { type: "string", usage: "Local server port, defaults to 5000", shortcut: "p", }, host: { type: "string", usage: "Server host, defaults to 'localhost'", }, "disable-threading": { type: "boolean", usage: "Disables multi-threaded mode", }, "num-processes": { type: "string", usage: "Number of processes for server, defaults to 1", }, ssl: { type: "boolean", usage: "Enable local serving using HTTPS", }, "ssl-pub": { type: "string", usage: "local ssl pem file to use for ssl", }, "ssl-pri": { type: "string", usage: "local ssl pem file to use for ssl private key", }, }, }, install: { usage: "Install WSGI handler and requirements for local use", lifecycleEvents: ["install"], }, clean: { usage: "Remove cached requirements", lifecycleEvents: ["clean"], }, command: { usage: "Execute shell commands or scripts remotely", lifecycleEvents: ["command"], options: { command: { type: "string", usage: "Command to execute", shortcut: "c", }, file: { type: "string", usage: "Path to a shell script to execute", shortcut: "f", }, }, commands: { local: { usage: "Execute shell commands or scripts locally", lifecycleEvents: ["command"], options: { command: { type: "string", usage: "Command to execute", shortcut: "c", }, file: { type: "string", usage: "Path to a shell script to execute", shortcut: "f", }, }, }, }, }, exec: { usage: "Evaluate Python code remotely", lifecycleEvents: ["exec"], options: { command: { type: "string", usage: "Python code to execute", shortcut: "c", }, file: { type: "string", usage: "Path to a Python script to execute", shortcut: "f", }, }, commands: { local: { usage: "Evaluate Python code locally", lifecycleEvents: ["exec"], options: { command: { type: "string", usage: "Python code to execute", shortcut: "c", }, file: { type: "string", usage: "Path to a Python script to execute", shortcut: "f", }, }, }, }, }, manage: { usage: "Run Django management commands remotely", lifecycleEvents: ["manage"], options: { command: { type: "string", usage: "Management command", shortcut: "c", required: true, }, }, commands: { local: { usage: "Run Django management commands locally", lifecycleEvents: ["manage"], options: { command: { type: "string", usage: "Management command", shortcut: "c", required: true, }, }, }, }, }, flask: { usage: "Run Flask CLI commands remotely", lifecycleEvents: ["flask"], options: { command: { type: "string", usage: "Flask CLI command", shortcut: "c", required: true, }, }, commands: { local: { usage: "Run Flask CLI commands locally", lifecycleEvents: ["flask"], options: { command: { type: "string", usage: "Flask CLI command", shortcut: "c", required: true, }, }, }, }, }, }, }, }; const deployBeforeHook = () => BbPromise.bind(this) .then(this.validate) .then(this.configurePackaging) .then(this.locatePython) .then(this.packWsgiHandler) .then(this.packRequirements) .then(this.linkRequirements) .then(this.checkWerkzeugPresent); const deployBeforeHookWithoutHandler = () => BbPromise.bind(this) .then(this.validate) .then(this.configurePackaging) .then(this.locatePython) .then(this.packRequirements) .then(this.linkRequirements); const deployAfterHook = () => BbPromise.bind(this) .then(this.validate) .then(this.unlinkRequirements) .then(this.cleanup); this.hooks = { "wsgi:wsgi": () => { this.serverless.cli.generateCommandsHelp(["wsgi"]); return BbPromise.resolve(); }, "wsgi:serve:serve": () => BbPromise.bind(this) .then(this.validate) .then(this.locatePython) .then(this.loadEnvVars) .then(this.serve), "wsgi:install:install": deployBeforeHook, "wsgi:command:command": () => BbPromise.bind(this) .then(this.validate) .then(() => this.command(false)), "wsgi:command:local:command": () => BbPromise.bind(this) .then(this.validate) .then(() => this.command(true)), "wsgi:exec:exec": () => BbPromise.bind(this) .then(this.validate) .then(() => this.exec(false)), "wsgi:exec:local:exec": () => BbPromise.bind(this) .then(this.validate) .then(() => this.exec(true)), "wsgi:manage:manage": () => BbPromise.bind(this) .then(this.validate) .then(() => this.manage(false)), "wsgi:manage:local:manage": () => BbPromise.bind(this) .then(this.validate) .then(() => this.manage(true)), "wsgi:flask:flask": () => BbPromise.bind(this) .then(this.validate) .then(() => this.flask(false)), "wsgi:flask:local:flask": () => BbPromise.bind(this) .then(this.validate) .then(() => this.flask(true)), "wsgi:clean:clean": () => deployAfterHook().then(this.cleanRequirements), "before:package:createDeploymentArtifacts": deployBeforeHook, "after:package:createDeploymentArtifacts": deployAfterHook, "before:deploy:function:packageFunction": () => { if ( _.includes(this.options.functionObj.handler, "wsgi_handler.handler") ) { return deployBeforeHook(); } else { return deployBeforeHookWithoutHandler(); } }, "after:deploy:function:packageFunction": deployAfterHook, "before:offline:start:init": deployBeforeHook, "after:offline:start:end": deployAfterHook, "before:invoke:local:invoke": () => { const functionObj = this.serverless.service.getFunction( this.options.function ); return BbPromise.bind(this) .then(this.validate) .then(() => { if (_.includes(functionObj.handler, "wsgi_handler.handler")) { return this.packWsgiHandler(false); } else { return BbPromise.resolve(); } }); }, "after:invoke:local:invoke": () => BbPromise.bind(this).then(this.validate).then(this.cleanup), }; } } module.exports = ServerlessWSGI;