diff --git a/example/service/handler.js b/example/service/handler.js index f3f54b9..aa7d49a 100644 --- a/example/service/handler.js +++ b/example/service/handler.js @@ -1,7 +1,7 @@ -'use strict'; +"use strict"; module.exports.hello = (event, context, callback) => { process.stdout.write(event.Records[0].EventSource); process.stdout.write(event.Records[0].Sns.Message); - callback(null, { message: 'Hello from SNS!', event }); + callback(null, { message: "Hello from SNS!", event }); }; diff --git a/package-lock.json b/package-lock.json index 368deef..da1d8ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "chai": "^4.1.2", "chai-string": "^1.4.0", "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "fs-extra": "^7.0.0", "jasmine": "^3.2.0", @@ -3241,6 +3242,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -11939,6 +11952,13 @@ } } }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "requires": {} + }, "eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", diff --git a/package.json b/package.json index 0b53f04..50b0234 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "chai": "^4.1.2", "chai-string": "^1.4.0", "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "fs-extra": "^7.0.0", "jasmine": "^3.2.0", diff --git a/spec/helpers/services.js b/spec/helpers/services.js index 05d4074..c8edfa5 100644 --- a/spec/helpers/services.js +++ b/spec/helpers/services.js @@ -1,60 +1,64 @@ -const tempy = require('tempy'); -const execSync = require('child_process').execSync; -const YAML = require('json2yaml'); -const fs = require('fs-extra'); -const path = require('path'); -const serverlessExec = path.join(__dirname, '../../node_modules/.bin/serverless'); -const packageJson = require('../../package.json') -const rimraf = require('rimraf') +const tempy = require("tempy"); +const execSync = require("child_process").execSync; +const YAML = require("json2yaml"); +const fs = require("fs-extra"); +const path = require("path"); +const serverlessExec = path.join( + __dirname, + "../../node_modules/.bin/serverless", +); +const packageJson = require("../../package.json"); +const rimraf = require("rimraf"); const debug = false; const defaultConfig = { - service: 'aws-nodejs', + service: "aws-nodejs", provider: { - name: 'aws', - runtime: 'nodejs12.x', - lambdaHashingVersion: '20201221', + name: "aws", + runtime: "nodejs12.x", + lambdaHashingVersion: "20201221", environment: { - LAMBDA_STAGE: '${ssm:/${opt:stage, self:provider.stage}/lambda/common/LAMBDA_STAGE}' - } + LAMBDA_STAGE: + "${ssm:/${opt:stage, self:provider.stage}/lambda/common/LAMBDA_STAGE}", + }, }, - plugins: [ - 'serverless-localstack' - ], + plugins: ["serverless-localstack"], custom: { localstack: { - host: 'http://localhost', + host: "http://localhost", debug: debug, - } + }, }, functions: { hello: { - handler: 'handler.hello' - } - } + handler: "handler.hello", + }, + }, }; const installPlugin = (dir) => { - const pluginsDir = path.join(dir, '.serverless_plugins'); + const pluginsDir = path.join(dir, ".serverless_plugins"); fs.mkdirsSync(pluginsDir); - execSync(`mkdir -p node_modules`, {cwd: dir}); - execSync(`ln -s ${__dirname}/../../ node_modules/${packageJson.name}`, {cwd: dir}); + execSync(`mkdir -p node_modules`, { cwd: dir }); + execSync(`ln -s ${__dirname}/../../ node_modules/${packageJson.name}`, { + cwd: dir, + }); }; -const execServerless = (arguments, dir) => { +const execServerless = (myArguments, dir) => { process.chdir(dir); - execSync(`${serverlessExec} ${arguments}`, { - stdio: 'inherit', - stderr: 'inherit', + execSync(`${serverlessExec} ${myArguments}`, { + stdio: "inherit", + stderr: "inherit", env: Object.assign({}, process.env, { AWS_ACCESS_KEY_ID: 1234, AWS_SECRET_ACCESS_KEY: 1234, PATH: process.env.PATH, - SLS_DEBUG: debug ? '*' : '' - }) + SLS_DEBUG: debug ? "*" : "", + }), }); }; @@ -62,7 +66,7 @@ exports.createService = (config, dir) => { dir = dir || tempy.directory(); config = Object.assign({}, defaultConfig, config); - execServerless('create --template aws-nodejs', dir); + execServerless("create --template aws-nodejs", dir); fs.writeFileSync(`${dir}/serverless.yml`, YAML.stringify(config)); installPlugin(dir); @@ -71,9 +75,9 @@ exports.createService = (config, dir) => { }; exports.deployService = (dir) => { - execServerless('deploy', dir); + execServerless("deploy", dir); }; exports.removeService = (dir) => { - rimraf.sync(dir) + rimraf.sync(dir); }; diff --git a/spec/integration/integration.spec.js b/spec/integration/integration.spec.js index 678ffbc..41cbb12 100644 --- a/spec/integration/integration.spec.js +++ b/spec/integration/integration.spec.js @@ -1,43 +1,44 @@ -'use strict'; +"use strict"; -const services = require('../helpers/services'); +const services = require("../helpers/services"); const LONG_TIMEOUT = 30000; -const AWS = require('aws-sdk'); +const AWS = require("aws-sdk"); // Set the region and endpoint in the config for LocalStack AWS.config.update({ - region: 'us-east-1', - endpoint: 'http://127.0.0.1:4566' + region: "us-east-1", + endpoint: "http://127.0.0.1:4566", }); AWS.config.credentials = new AWS.Credentials({ - accessKeyId: 'test', - secretAccessKey: 'test', + accessKeyId: "test", + secretAccessKey: "test", }); const ssm = new AWS.SSM(); const params = { - Name: '/dev/lambda/common/LAMBDA_STAGE', - Type: 'String', - Value: 'my-value', - Overwrite: true + Name: "/dev/lambda/common/LAMBDA_STAGE", + Type: "String", + Value: "my-value", + Overwrite: true, }; -describe('LocalstackPlugin', () => { - - beforeEach( - async () => { - await ssm.putParameter(params).promise(); - this.service = services.createService({}); +describe("LocalstackPlugin", () => { + beforeEach(async () => { + await ssm.putParameter(params).promise(); + this.service = services.createService({}); }); - afterEach( () => { + afterEach(() => { services.removeService(this.service); }); - it('should deploy a stack', () => { - services.deployService(this.service); - }, LONG_TIMEOUT); - -}); \ No newline at end of file + it( + "should deploy a stack", + () => { + services.deployService(this.service); + }, + LONG_TIMEOUT, + ); +}); diff --git a/spec/unit/index.spec.js b/spec/unit/index.spec.js index 62efb7a..609a491 100644 --- a/spec/unit/index.spec.js +++ b/spec/unit/index.spec.js @@ -1,24 +1,23 @@ -'use strict'; -const LocalstackPlugin = require('../../src/index'); -const chai = require('chai'); -const expect = require('chai').expect; -const sinon = require('sinon'); -const AWS = require('aws-sdk'); -const Serverless = require('serverless') +"use strict"; +const LocalstackPlugin = require("../../src/index"); +const chai = require("chai"); +const expect = require("chai").expect; +const sinon = require("sinon"); +const AWS = require("aws-sdk"); +const Serverless = require("serverless"); let AwsProvider; try { - AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider'); + AwsProvider = require("serverless/lib/plugins/aws/provider/awsProvider"); } catch (e) { - AwsProvider = require('serverless/lib/plugins/aws/provider'); + AwsProvider = require("serverless/lib/plugins/aws/provider"); } -chai.use(require('chai-string')); +chai.use(require("chai-string")); // Enable for more verbose logging const debug = false; describe("LocalstackPlugin", () => { - let serverless; let awsProvider; let awsConfig; @@ -26,26 +25,26 @@ describe("LocalstackPlugin", () => { let sandbox; let defaultPluginState = {}; let config = { - host: 'http://localhost', - debug: debug + host: "http://localhost", + debug: debug, }; beforeEach(() => { sandbox = sinon.createSandbox(); - serverless = new Serverless({commands: ['deploy'], options: {}}); + serverless = new Serverless({ commands: ["deploy"], options: {} }); awsProvider = new AwsProvider(serverless, {}); awsConfig = new AWS.Config(); AWS.config = awsConfig; awsProvider.sdk = AWS; awsProvider.config = awsConfig; serverless.init(); - serverless.setProvider('aws', awsProvider); + serverless.setProvider("aws", awsProvider); if (serverless.cli) { serverless.cli.log = () => { if (debug) { - console.log.apply(this, arguments); // eslint-disable-line no-console + console.log.apply(this, arguments); // eslint-disable-line no-console } - } + }; } }); @@ -53,102 +52,107 @@ describe("LocalstackPlugin", () => { sandbox.restore(); }); - let simulateBeforeDeployHooks = async function(instance) { + let simulateBeforeDeployHooks = async function (instance) { instance.readConfig(); instance.activatePlugin(); instance.getStageVariable(); await instance.reconfigureAWS(); }; - describe('#constructor()', () => { - describe('with empty configuration', () => { + describe("#constructor()", () => { + describe("with empty configuration", () => { beforeEach(async () => { serverless.service.custom = {}; instance = new LocalstackPlugin(serverless, defaultPluginState); await simulateBeforeDeployHooks(instance); }); - it('should not set the endpoints', () => { + it("should not set the endpoints", () => { expect(instance.endpoints).to.be.empty; }); - it('should not set the endpoint file', () => { + it("should not set the endpoint file", () => { expect(instance.endpointFile).to.be.undefined; }); }); - describe('with config file provided', () => { + describe("with config file provided", () => { beforeEach(async () => { serverless.service.custom = { - localstack: {} + localstack: {}, }; instance = new LocalstackPlugin(serverless, defaultPluginState); await simulateBeforeDeployHooks(instance); }); - it('should not set the endpoints if the stages config option does not include the deployment stage', async () => { - serverless.service.custom.localstack.stages = ['production']; + it("should not set the endpoints if the stages config option does not include the deployment stage", async () => { + serverless.service.custom.localstack.stages = ["production"]; - let plugin = new LocalstackPlugin(serverless, defaultPluginState); - await simulateBeforeDeployHooks(plugin); - expect(plugin.endpoints).to.be.empty; + let plugin = new LocalstackPlugin(serverless, defaultPluginState); + await simulateBeforeDeployHooks(plugin); + expect(plugin.endpoints).to.be.empty; }); - it('should set the endpoints if the stages config option includes the deployment stage', async () => { - serverless.service.custom.localstack.stages = ['production', 'staging']; + it("should set the endpoints if the stages config option includes the deployment stage", async () => { + serverless.service.custom.localstack.stages = ["production", "staging"]; - let plugin = new LocalstackPlugin(serverless, {'stage':'production'}) + let plugin = new LocalstackPlugin(serverless, { stage: "production" }); await simulateBeforeDeployHooks(plugin); - expect(plugin.config.stages).to.deep.equal(['production','staging']); - expect(plugin.config.stage).to.equal('production'); + expect(plugin.config.stages).to.deep.equal(["production", "staging"]); + expect(plugin.config.stage).to.equal("production"); }); - it('should fail if the endpoint file does not exist and the stages config option includes the deployment stage', () => { + it("should fail if the endpoint file does not exist and the stages config option includes the deployment stage", () => { serverless.service.custom.localstack = { - endpointFile: 'missing.json', - stages: ['production'] - } + endpointFile: "missing.json", + stages: ["production"], + }; let plugin = () => { - let pluginInstance = new LocalstackPlugin(serverless, {'stage':'production'}); + let pluginInstance = new LocalstackPlugin(serverless, { + stage: "production", + }); pluginInstance.readConfig(); - } + }; - expect(plugin).to.throw('Endpoint file "missing.json" is invalid:') + expect(plugin).to.throw('Endpoint file "missing.json" is invalid:'); }); - it('should not fail if the endpoint file does not exist when the stages config option does not include the deployment stage', () => { + it("should not fail if the endpoint file does not exist when the stages config option does not include the deployment stage", () => { serverless.service.custom.localstack = { - endpointFile: 'missing.json', - stages: ['production'] - } + endpointFile: "missing.json", + stages: ["production"], + }; let plugin = () => { - let pluginInstance = new LocalstackPlugin(serverless, {'stage':'staging'}); + let pluginInstance = new LocalstackPlugin(serverless, { + stage: "staging", + }); pluginInstance.readConfig(); - } + }; - expect(plugin).to.not.throw('Endpoint file "missing.json" is invalid:') + expect(plugin).to.not.throw('Endpoint file "missing.json" is invalid:'); }); - it('should fail if the endpoint file is not json', () => { + it("should fail if the endpoint file is not json", () => { serverless.service.custom.localstack = { - endpointFile: 'README.md' - } + endpointFile: "README.md", + }; let plugin = () => { - let pluginInstance = new LocalstackPlugin(serverless, defaultPluginState); + let pluginInstance = new LocalstackPlugin( + serverless, + defaultPluginState, + ); pluginInstance.readConfig(); - } - expect(plugin).to.throw(/Endpoint file "README.md" is invalid:/) + }; + expect(plugin).to.throw(/Endpoint file "README.md" is invalid:/); }); - }); }); - describe('#request() bound on AWS provider', () => { - - beforeEach(()=> { + describe("#request() bound on AWS provider", () => { + beforeEach(() => { class FakeService { foo() { return this; @@ -161,56 +165,58 @@ describe("LocalstackPlugin", () => { serverless.providers.aws.sdk.S3 = FakeService; serverless.service.custom = { - localstack: {} - } + localstack: {}, + }; }); - it('should overwrite the S3 hostname', async () => { - const pathToTemplate = 'https://s3.amazonaws.com/path/to/template'; - const request = sinon.stub(awsProvider, 'request'); + it("should overwrite the S3 hostname", async () => { + const pathToTemplate = "https://s3.amazonaws.com/path/to/template"; + const request = sinon.stub(awsProvider, "request"); instance = new LocalstackPlugin(serverless, defaultPluginState); await simulateBeforeDeployHooks(instance); - await awsProvider.request('s3', 'foo', { - TemplateURL: pathToTemplate + await awsProvider.request("s3", "foo", { + TemplateURL: pathToTemplate, }); expect(request.called).to.be.true; let templateUrl = request.firstCall.args[2].TemplateURL; // url should either start with 'http://localhost' or 'http://127.0.0.1 - expect(templateUrl).to.satisfy((url) => url === `${config.host}:4566/path/to/template` || url === 'http://127.0.0.1:4566/path/to/template'); + expect(templateUrl).to.satisfy( + (url) => + url === `${config.host}:4566/path/to/template` || + url === "http://127.0.0.1:4566/path/to/template", + ); }); - it('should overwrite the S3 hostname with the value from environment variable', async () => { + it("should overwrite the S3 hostname with the value from environment variable", async () => { // Set environment variable to overwrite the default host - process.env.AWS_ENDPOINT_URL = 'http://localstack:4566'; + process.env.AWS_ENDPOINT_URL = "http://localstack:4566"; - const pathToTemplate = 'https://s3.amazonaws.com/path/to/template'; - const request = sinon.stub(awsProvider, 'request'); + const pathToTemplate = "https://s3.amazonaws.com/path/to/template"; + const request = sinon.stub(awsProvider, "request"); instance = new LocalstackPlugin(serverless, defaultPluginState); await simulateBeforeDeployHooks(instance); - await awsProvider.request('s3', 'foo', { - TemplateURL: pathToTemplate + await awsProvider.request("s3", "foo", { + TemplateURL: pathToTemplate, }); // Remove the environment variable again to not affect other tests - delete process.env.AWS_ENDPOINT_URL + delete process.env.AWS_ENDPOINT_URL; expect(request.called).to.be.true; let templateUrl = request.firstCall.args[2].TemplateURL; - expect(templateUrl).to.equal('http://localstack:4566/path/to/template'); + expect(templateUrl).to.equal("http://localstack:4566/path/to/template"); }); - it('should not send validateTemplate calls to localstack', async () => { - const request = sinon.stub(awsProvider, 'request'); - instance = new LocalstackPlugin(serverless, defaultPluginState) + it("should not send validateTemplate calls to localstack", async () => { + const request = sinon.stub(awsProvider, "request"); + instance = new LocalstackPlugin(serverless, defaultPluginState); await simulateBeforeDeployHooks(instance); - await awsProvider.request('S3', 'validateTemplate', {}); + await awsProvider.request("S3", "validateTemplate", {}); expect(request.called).to.be.false; }); - }); - -}) +}); diff --git a/src/index.js b/src/index.js index 17b586b..2933675 100644 --- a/src/index.js +++ b/src/index.js @@ -1,25 +1,25 @@ -'use strict'; +"use strict"; const AdmZip = require("adm-zip"); -const AWS = require('aws-sdk'); -const fs = require('fs'); -const path = require('path'); -const net = require('net'); -const {promisify} = require('es6-promisify'); -const exec = promisify(require('child_process').exec); +const AWS = require("aws-sdk"); +const fs = require("fs"); +const path = require("path"); +const net = require("net"); +const { promisify } = require("es6-promisify"); +const exec = promisify(require("child_process").exec); // Default stage used by Serverless -const DEFAULT_STAGE = 'dev'; +const DEFAULT_STAGE = "dev"; // Strings or other values considered to represent "true" -const TRUE_VALUES = ['1', 'true', true]; +const TRUE_VALUES = ["1", "true", true]; // Plugin naming and build directory of serverless-plugin-typescript plugin -const TS_PLUGIN_TSC = 'TypeScriptPlugin' -const TYPESCRIPT_PLUGIN_BUILD_DIR_TSC = '.build'; //TODO detect from tsconfig.json +const TS_PLUGIN_TSC = "TypeScriptPlugin"; +const TYPESCRIPT_PLUGIN_BUILD_DIR_TSC = ".build"; //TODO detect from tsconfig.json // Plugin naming and build directory of serverless-webpack plugin -const TS_PLUGIN_WEBPACK = 'ServerlessWebpack' -const TYPESCRIPT_PLUGIN_BUILD_DIR_WEBPACK = '.webpack/service'; //TODO detect from webpack.config.js +const TS_PLUGIN_WEBPACK = "ServerlessWebpack"; +const TYPESCRIPT_PLUGIN_BUILD_DIR_WEBPACK = ".webpack/service"; //TODO detect from webpack.config.js // Plugin naming and build directory of serverless-webpack plugin -const TS_PLUGIN_ESBUILD = 'EsbuildServerlessPlugin' -const TYPESCRIPT_PLUGIN_BUILD_DIR_ESBUILD = '.esbuild/.build'; //TODO detect from esbuild.config.js +const TS_PLUGIN_ESBUILD = "EsbuildServerlessPlugin"; +const TYPESCRIPT_PLUGIN_BUILD_DIR_ESBUILD = ".esbuild/.build"; //TODO detect from esbuild.config.js // Default AWS endpoint URL const DEFAULT_AWS_ENDPOINT_URL = "http://localhost:4566"; @@ -31,103 +31,113 @@ const awsEndpointUrl = process.env.AWS_ENDPOINT_URL || DEFAULT_AWS_ENDPOINT_URL; class LocalstackPlugin { constructor(serverless, options) { - this.serverless = serverless; this.options = options; - this.hooks = {'initialize': () => this.init()}; + this.hooks = { initialize: () => this.init() }; // Define a before-hook for all event types for (let event in this.serverless.pluginManager.hooks) { - const doAdd = event.startsWith('before:'); + const doAdd = event.startsWith("before:"); if (doAdd && !this.hooks[event]) { this.hooks[event] = this.beforeEventHook.bind(this); } } // Define a hook for aws:info to fix output data - this.hooks['aws:info:gatherData'] = this.fixOutputEndpoints.bind(this); + this.hooks["aws:info:gatherData"] = this.fixOutputEndpoints.bind(this); // Define a hook for deploy:deploy to fix handler location for mounted lambda - this.addHookInFirstPosition('deploy:deploy', this.patchTypeScriptPluginMountedCodeLocation); + this.addHookInFirstPosition( + "deploy:deploy", + this.patchTypeScriptPluginMountedCodeLocation, + ); // Add a before hook for aws:common:validate and make sure it is in the very first position - this.addHookInFirstPosition('before:aws:common:validate:validate', this.beforeEventHook); + this.addHookInFirstPosition( + "before:aws:common:validate:validate", + this.beforeEventHook, + ); // Add a hook to fix TypeError when accessing undefined state attribute - this.addHookInFirstPosition('before:aws:deploy:deploy:checkForChanges', this.beforeDeployCheckForChanges); + this.addHookInFirstPosition( + "before:aws:deploy:deploy:checkForChanges", + this.beforeDeployCheckForChanges, + ); - const compileEventsHooks = this.serverless.pluginManager.hooks['package:compileEvents'] || []; + const compileEventsHooks = + this.serverless.pluginManager.hooks["package:compileEvents"] || []; compileEventsHooks.push({ - pluginName: 'LocalstackPlugin', hook: this.patchCustomResourceLambdaS3ForcePathStyle.bind(this) + pluginName: "LocalstackPlugin", + hook: this.patchCustomResourceLambdaS3ForcePathStyle.bind(this), }); this.awsServices = [ - 'acm', - 'amplify', - 'apigateway', - 'apigatewayv2', - 'application-autoscaling', - 'appsync', - 'athena', - 'autoscaling', - 'batch', - 'cloudformation', - 'cloudfront', - 'cloudsearch', - 'cloudtrail', - 'cloudwatch', - 'cloudwatchlogs', - 'codecommit', - 'cognito-idp', - 'cognito-identity', - 'docdb', - 'dynamodb', - 'dynamodbstreams', - 'ec2', - 'ecr', - 'ecs', - 'eks', - 'elasticache', - 'elasticbeanstalk', - 'elb', - 'elbv2', - 'emr', - 'es', - 'events', - 'firehose', - 'glacier', - 'glue', - 'iam', - 'iot', - 'iotanalytics', - 'iotevents', - 'iot-data', - 'iot-jobs-data', - 'kafka', - 'kinesis', - 'kinesisanalytics', - 'kms', - 'lambda', - 'logs', - 'mediastore', - 'neptune', - 'organizations', - 'qldb', - 'rds', - 'redshift', - 'route53', - 's3', - 's3control', - 'sagemaker', - 'sagemaker-runtime', - 'secretsmanager', - 'ses', - 'sns', - 'sqs', - 'ssm', - 'stepfunctions', - 'sts', - 'timestream', - 'transfer', - 'xray', + "acm", + "amplify", + "apigateway", + "apigatewayv2", + "application-autoscaling", + "appsync", + "athena", + "autoscaling", + "batch", + "cloudformation", + "cloudfront", + "cloudsearch", + "cloudtrail", + "cloudwatch", + "cloudwatchlogs", + "codecommit", + "cognito-idp", + "cognito-identity", + "docdb", + "dynamodb", + "dynamodbstreams", + "ec2", + "ecr", + "ecs", + "eks", + "elasticache", + "elasticbeanstalk", + "elb", + "elbv2", + "emr", + "es", + "events", + "firehose", + "glacier", + "glue", + "iam", + "iot", + "iotanalytics", + "iotevents", + "iot-data", + "iot-jobs-data", + "kafka", + "kinesis", + "kinesisanalytics", + "kms", + "lambda", + "logs", + "mediastore", + "neptune", + "organizations", + "qldb", + "rds", + "redshift", + "route53", + "s3", + "s3control", + "sagemaker", + "sagemaker-runtime", + "secretsmanager", + "ses", + "sns", + "sqs", + "ssm", + "stepfunctions", + "sts", + "timestream", + "transfer", + "xray", ]; // Activate the synchronous parts of plugin config here in the constructor, but @@ -136,31 +146,37 @@ class LocalstackPlugin { // If we're using webpack, we need to make sure we retain the compiler output directory if (this.detectTypescriptPluginType() === TS_PLUGIN_WEBPACK) { - const p = this.serverless.pluginManager.plugins.find((x) => x.constructor.name === TS_PLUGIN_WEBPACK); + const p = this.serverless.pluginManager.plugins.find( + (x) => x.constructor.name === TS_PLUGIN_WEBPACK, + ); if ( - this.shouldMountCode() && ( - !p || - !p.serverless || - !p.serverless.configurationInput || - !p.serverless.configurationInput.custom || - !p.serverless.configurationInput.custom.webpack || - !p.serverless.configurationInput.custom.webpack.keepOutputDirectory - ) + this.shouldMountCode() && + (!p || + !p.serverless || + !p.serverless.configurationInput || + !p.serverless.configurationInput.custom || + !p.serverless.configurationInput.custom.webpack || + !p.serverless.configurationInput.custom.webpack.keepOutputDirectory) ) { - throw new Error('When mounting Lambda code, you must retain webpack output directory. ' - + 'Set custom.webpack.keepOutputDirectory to true.'); + throw new Error( + "When mounting Lambda code, you must retain webpack output directory. " + + "Set custom.webpack.keepOutputDirectory to true.", + ); } } } async init() { - await this.reconfigureAWS() + await this.reconfigureAWS(); } addHookInFirstPosition(eventName, hookFunction) { - this.serverless.pluginManager.hooks[eventName] = this.serverless.pluginManager.hooks[eventName] || []; - this.serverless.pluginManager.hooks[eventName].unshift( - { pluginName: 'LocalstackPlugin', hook: hookFunction.bind(this, eventName) }); + this.serverless.pluginManager.hooks[eventName] = + this.serverless.pluginManager.hooks[eventName] || []; + this.serverless.pluginManager.hooks[eventName].unshift({ + pluginName: "LocalstackPlugin", + hook: hookFunction.bind(this, eventName), + }); } activatePlugin(preHooks) { @@ -185,35 +201,57 @@ class LocalstackPlugin { const functionObject = this.serverless.service.getFunction(functionName); functionObject.package = functionObject.package || {}; functionObject.package.artifact = __filename; - return compileFunction._functionOriginal.apply(null, arguments).then(() => { - const resources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources; - Object.keys(resources).forEach(id => { - const res = resources[id]; - if (res.Type === 'AWS::Lambda::Function') { - res.Properties.Code.S3Bucket = process.env.BUCKET_MARKER_LOCAL || 'hot-reload'; // default changed to 'hot-reload' with LS v2 release - res.Properties.Code.S3Key = process.cwd(); - const mountCode = this.config.lambda.mountCode; - if (typeof mountCode === 'string' && mountCode.toLowerCase() !== 'true') { - res.Properties.Code.S3Key = path.join(res.Properties.Code.S3Key, this.config.lambda.mountCode); - } - if (process.env.LAMBDA_MOUNT_CWD) { - // Allow users to define a custom working directory for Lambda mounts. - // For example, when deploying a Serverless app in a Linux VM (that runs Docker) on a - // Windows host where the "-v :" flag to "docker run" requires us - // to specify a "local_dir" relative to the Windows host file system that is mounted - // into the VM (e.g., "c:/users/guest/..."). - res.Properties.Code.S3Key = process.env.LAMBDA_MOUNT_CWD; + return compileFunction._functionOriginal + .apply(null, arguments) + .then(() => { + const resources = + this.serverless.service.provider.compiledCloudFormationTemplate + .Resources; + Object.keys(resources).forEach((id) => { + const res = resources[id]; + if (res.Type === "AWS::Lambda::Function") { + res.Properties.Code.S3Bucket = + process.env.BUCKET_MARKER_LOCAL || "hot-reload"; // default changed to 'hot-reload' with LS v2 release + res.Properties.Code.S3Key = process.cwd(); + const mountCode = this.config.lambda.mountCode; + if ( + typeof mountCode === "string" && + mountCode.toLowerCase() !== "true" + ) { + res.Properties.Code.S3Key = path.join( + res.Properties.Code.S3Key, + this.config.lambda.mountCode, + ); + } + if (process.env.LAMBDA_MOUNT_CWD) { + // Allow users to define a custom working directory for Lambda mounts. + // For example, when deploying a Serverless app in a Linux VM (that runs Docker) on a + // Windows host where the "-v :" flag to "docker run" requires us + // to specify a "local_dir" relative to the Windows host file system that is mounted + // into the VM (e.g., "c:/users/guest/..."). + res.Properties.Code.S3Key = process.env.LAMBDA_MOUNT_CWD; + } } - } + }); }); - }); } - this.skipIfMountLambda('AwsCompileFunctions', 'compileFunction', compileFunction); - this.skipIfMountLambda('AwsCompileFunctions', 'downloadPackageArtifacts'); - this.skipIfMountLambda('AwsDeploy', 'extendedValidate'); + this.skipIfMountLambda( + "AwsCompileFunctions", + "compileFunction", + compileFunction, + ); + this.skipIfMountLambda("AwsCompileFunctions", "downloadPackageArtifacts"); + this.skipIfMountLambda("AwsDeploy", "extendedValidate"); if (this.detectTypescriptPluginType()) { - this.skipIfMountLambda(this.detectTypescriptPluginType(), 'cleanup', null, [ - 'after:package:createDeploymentArtifacts', 'after:deploy:function:packageFunction']); + this.skipIfMountLambda( + this.detectTypescriptPluginType(), + "cleanup", + null, + [ + "after:package:createDeploymentArtifacts", + "after:deploy:function:packageFunction", + ], + ); } this.pluginActivated = true; @@ -232,7 +270,7 @@ class LocalstackPlugin { beforeDeployCheckForChanges() { // patch to avoid "TypeError: reading 'console' of undefined" in plugins/aws/deploy/index.js on Sls v3.17.0+ - const plugin = this.findPlugin('AwsDeploy'); + const plugin = this.findPlugin("AwsDeploy"); if (plugin) { plugin.state = plugin.state || {}; } @@ -242,52 +280,58 @@ class LocalstackPlugin { // reconfigure AWS endpoints based on current stage variables this.getStageVariable(); - return this.startLocalStack().then( - () => { - this.patchServerlessSecrets(); - this.patchS3BucketLocationResponse(); - this.patchS3CreateBucketLocationConstraint(); - } - ); + return this.startLocalStack().then(() => { + this.patchServerlessSecrets(); + this.patchS3BucketLocationResponse(); + this.patchS3CreateBucketLocationConstraint(); + }); } // Convenience method for detecting JS/TS transpiler detectTypescriptPluginType() { - if (this.findPlugin(TS_PLUGIN_TSC)) return TS_PLUGIN_TSC - if (this.findPlugin(TS_PLUGIN_WEBPACK)) return TS_PLUGIN_WEBPACK - if (this.findPlugin(TS_PLUGIN_ESBUILD)) return TS_PLUGIN_ESBUILD - return undefined + if (this.findPlugin(TS_PLUGIN_TSC)) return TS_PLUGIN_TSC; + if (this.findPlugin(TS_PLUGIN_WEBPACK)) return TS_PLUGIN_WEBPACK; + if (this.findPlugin(TS_PLUGIN_ESBUILD)) return TS_PLUGIN_ESBUILD; + return undefined; } // Convenience method for getting build directory of installed JS/TS transpiler getTSBuildDir() { - const TS_PLUGIN = this.detectTypescriptPluginType() - if (TS_PLUGIN === TS_PLUGIN_TSC) return TYPESCRIPT_PLUGIN_BUILD_DIR_TSC - if (TS_PLUGIN === TS_PLUGIN_WEBPACK) return TYPESCRIPT_PLUGIN_BUILD_DIR_WEBPACK - if (TS_PLUGIN === TS_PLUGIN_ESBUILD) return TYPESCRIPT_PLUGIN_BUILD_DIR_ESBUILD - return undefined + const TS_PLUGIN = this.detectTypescriptPluginType(); + if (TS_PLUGIN === TS_PLUGIN_TSC) return TYPESCRIPT_PLUGIN_BUILD_DIR_TSC; + if (TS_PLUGIN === TS_PLUGIN_WEBPACK) + return TYPESCRIPT_PLUGIN_BUILD_DIR_WEBPACK; + if (TS_PLUGIN === TS_PLUGIN_ESBUILD) + return TYPESCRIPT_PLUGIN_BUILD_DIR_ESBUILD; + return undefined; } findPlugin(name) { - return this.serverless.pluginManager.plugins.find(p => p.constructor.name === name); + return this.serverless.pluginManager.plugins.find( + (p) => p.constructor.name === name, + ); } skipIfMountLambda(pluginName, functionName, overrideFunction, hookNames) { const plugin = this.findPlugin(pluginName); if (!plugin) { - this.log('Warning: Unable to find plugin named: ' + pluginName) + this.log("Warning: Unable to find plugin named: " + pluginName); return; } if (!plugin[functionName]) { - this.log(`Unable to find function ${functionName} on plugin ${pluginName}`) + this.log( + `Unable to find function ${functionName} on plugin ${pluginName}`, + ); return; } const functionOriginal = plugin[functionName].bind(plugin); function overrideFunctionDefault() { if (this.shouldMountCode()) { - const fqn = pluginName + '.' + functionName; - this.log('Skip plugin function ' + fqn + ' (lambda.mountCode flag is enabled)'); + const fqn = pluginName + "." + functionName; + this.log( + "Skip plugin function " + fqn + " (lambda.mountCode flag is enabled)", + ); return Promise.resolve(); } return functionOriginal.apply(null, arguments); @@ -299,19 +343,15 @@ class LocalstackPlugin { plugin[functionName] = boundOverrideFunction; // overwrite bound functions for specified hook names - (hookNames || []).forEach( - (hookName) => { - plugin.hooks[hookName] = boundOverrideFunction; - const slsHooks = this.serverless.pluginManager.hooks[hookName] || []; - slsHooks.forEach( - (hookItem) => { - if (hookItem.pluginName === pluginName) { - hookItem.hook = boundOverrideFunction; - } - } - ); + (hookNames || []).forEach((hookName) => { + plugin.hooks[hookName] = boundOverrideFunction; + const slsHooks = this.serverless.pluginManager.hooks[hookName] || []; + slsHooks.forEach((hookItem) => { + if (hookItem.pluginName === pluginName) { + hookItem.hook = boundOverrideFunction; } - ); + }); + }); } readConfig(preHooks) { @@ -319,7 +359,8 @@ class LocalstackPlugin { return; } - const localstackConfig = (this.serverless.service.custom || {}).localstack || {}; + const localstackConfig = + (this.serverless.service.custom || {}).localstack || {}; this.config = Object.assign({}, this.options, localstackConfig); //Get the target deployment stage @@ -338,7 +379,7 @@ class LocalstackPlugin { this.loadEndpointsFromDisk(this.endpointFile); this._endpointFileLoaded = true; } catch (e) { - if (!this.endpointFile.includes('${')) { + if (!this.endpointFile.includes("${")) { throw e; } // Could be related to variable references not being resolved yet, and hence the endpoints file @@ -355,9 +396,12 @@ class LocalstackPlugin { // Activate the plugin if either: // (1) the serverless stage (explicitly defined or default stage "dev") is included in the `stages` config; or // (2) serverless is invoked without a --stage flag (default stage "dev") and no `stages` config is provided - const effectiveStage = this.options.stage || this.config.stage || DEFAULT_STAGE; - const noStageUsed = this.config.stages === undefined && effectiveStage == DEFAULT_STAGE; - const includedInStages = this.config.stages && this.config.stages.includes(effectiveStage); + const effectiveStage = + this.options.stage || this.config.stage || DEFAULT_STAGE; + const noStageUsed = + this.config.stages === undefined && effectiveStage == DEFAULT_STAGE; + const includedInStages = + this.config.stages && this.config.stages.includes(effectiveStage); return noStageUsed || includedInStages; } @@ -372,18 +416,19 @@ class LocalstackPlugin { getStageVariable() { const customConfig = this.serverless.service.custom || {}; const providerConfig = this.serverless.service.provider || {}; - this.debug('config.options_stage: ' + this.config.options_stage); - this.debug('serverless.service.custom.stage: ' + customConfig.stage); - this.debug('serverless.service.provider.stage: ' + providerConfig.stage); - this.config.stage = this.config.options_stage || customConfig.stage || providerConfig.stage; - this.debug('config.stage: ' + this.config.stage); + this.debug("config.options_stage: " + this.config.options_stage); + this.debug("serverless.service.custom.stage: " + customConfig.stage); + this.debug("serverless.service.provider.stage: " + providerConfig.stage); + this.config.stage = + this.config.options_stage || customConfig.stage || providerConfig.stage; + this.debug("config.stage: " + this.config.stage); } fixOutputEndpoints() { - if(!this.isActive()) { + if (!this.isActive()) { return; } - const plugin = this.findPlugin('AwsInfo'); + const plugin = this.findPlugin("AwsInfo"); const endpoints = plugin.gatheredData.info.endpoints || []; const edgePort = this.getEdgePort(); endpoints.forEach((entry, idx) => { @@ -395,13 +440,14 @@ class LocalstackPlugin { // - https://2e22431f.execute-api.us-east-1.localhost // - https://2e22431f.execute-api.us-east-1.localhost.localstack.cloud // - https://2e22431f.execute-api.us-east-1.amazonaws.com - const regex2 = /[^\s:]*:\/\/([^.]+)\.execute-api\.[^/]+(([^/]+)(\/.*)?)?\/*$/g; + const regex2 = + /[^\s:]*:\/\/([^.]+)\.execute-api\.[^/]+(([^/]+)(\/.*)?)?\/*$/g; const replace2 = `https://$1.execute-api.localhost.localstack.cloud:${edgePort}$2`; endpoints[idx] = entry.replace(regex2, replace2); }); // Replace ServerlessStepFunctions display - this.stepFunctionsReplaceDisplay() + this.stepFunctionsReplaceDisplay(); } /** @@ -413,20 +459,19 @@ class LocalstackPlugin { } const getContainer = () => { - return exec('docker ps').then( - (stdout) => { - const exists = stdout.split('\n').filter( - (line) => ( - line.indexOf('localstack/localstack') >= 0 || - line.indexOf('localstack/localstack-pro') >= 0 || - line.indexOf('localstack_localstack') >= 0 - ) - ); - if (exists.length) { - return exists[0].replace('\t', ' ').split(' ')[0]; - } - } - ) + return exec("docker ps").then((stdout) => { + const exists = stdout + .split("\n") + .filter( + (line) => + line.indexOf("localstack/localstack") >= 0 || + line.indexOf("localstack/localstack-pro") >= 0 || + line.indexOf("localstack_localstack") >= 0, + ); + if (exists.length) { + return exists[0].replace("\t", " ").split(" ")[0]; + } + }); }; const dockerStartupTimeoutMS = 1000 * 60 * 2; @@ -434,69 +479,76 @@ class LocalstackPlugin { const checkStatus = (containerID, timeout) => { timeout = timeout || Date.now() + dockerStartupTimeoutMS; if (Date.now() > timeout) { - this.log('Warning: Timeout when checking state of LocalStack container'); + this.log( + "Warning: Timeout when checking state of LocalStack container", + ); return; } return this.sleep(4000).then(() => { - this.log(`Checking state of LocalStack container ${containerID}`) - return exec(`docker logs "${containerID}"`).then( - (logs) => { - const ready = logs.split('\n').filter((line) => line.indexOf('Ready.') >= 0); - if (ready.length) { - return Promise.resolve(); - } - return checkStatus(containerID, timeout); - } - ); + this.log(`Checking state of LocalStack container ${containerID}`); + return exec(`docker logs "${containerID}"`).then((logs) => { + const ready = logs + .split("\n") + .filter((line) => line.indexOf("Ready.") >= 0); + if (ready.length) { + return Promise.resolve(); + } + return checkStatus(containerID, timeout); + }); }); - } + }; const addNetworks = async (containerID) => { - if(this.config.networks) { - for(var network in this.config.networks) { - await exec(`docker network connect "${this.config.networks[network]}" ${containerID}`); + if (this.config.networks) { + for (var network in this.config.networks) { + await exec( + `docker network connect "${this.config.networks[network]}" ${containerID}`, + ); } } return containerID; - } + }; const startContainer = () => { - this.log('Starting LocalStack in Docker. This can take a while.'); + this.log("Starting LocalStack in Docker. This can take a while."); const cwd = process.cwd(); const env = this.clone(process.env); - env.DEBUG = '1'; - env.LAMBDA_EXECUTOR = env.LAMBDA_EXECUTOR || 'docker'; - env.LAMBDA_REMOTE_DOCKER = env.LAMBDA_REMOTE_DOCKER || '0'; - env.DOCKER_FLAGS = (env.DOCKER_FLAGS || '') + ` -v ${cwd}:${cwd}`; - env.START_WEB = env.START_WEB || '0'; - const maxBuffer = (+env.EXEC_MAXBUFFER)||50*1000*1000; // 50mb buffer to handle output + env.DEBUG = "1"; + env.LAMBDA_EXECUTOR = env.LAMBDA_EXECUTOR || "docker"; + env.LAMBDA_REMOTE_DOCKER = env.LAMBDA_REMOTE_DOCKER || "0"; + env.DOCKER_FLAGS = (env.DOCKER_FLAGS || "") + ` -v ${cwd}:${cwd}`; + env.START_WEB = env.START_WEB || "0"; + const maxBuffer = +env.EXEC_MAXBUFFER || 50 * 1000 * 1000; // 50mb buffer to handle output if (this.shouldRunDockerSudo()) { - env.DOCKER_CMD = 'sudo docker'; + env.DOCKER_CMD = "sudo docker"; } - const options = {env: env, maxBuffer}; - return exec('localstack start -d', options).then(getContainer) - .then((containerID) => addNetworks(containerID)) - .then((containerID) => checkStatus(containerID)); - } + const options = { env: env, maxBuffer }; + return exec("localstack start -d", options) + .then(getContainer) + .then((containerID) => addNetworks(containerID)) + .then((containerID) => checkStatus(containerID)); + }; const startCompose = () => { - this.log('Starting LocalStack using the provided docker-compose file. This can take a while.'); - return exec(`docker-compose -f ${this.config.docker.compose_file} up -d`).then(getContainer) - } + this.log( + "Starting LocalStack using the provided docker-compose file. This can take a while.", + ); + return exec( + `docker-compose -f ${this.config.docker.compose_file} up -d`, + ).then(getContainer); + }; - return getContainer().then( - (containerID) => { - if(containerID) { - return; - } + return getContainer().then((containerID) => { + if (containerID) { + return; + } - if(this.config.docker && this.config.docker.compose_file){ - return startCompose(); - } + if (this.config.docker && this.config.docker.compose_file) { + return startCompose(); + } - return startContainer(); - } - ); + return startContainer(); + }); } /** @@ -504,19 +556,22 @@ class LocalstackPlugin { * used, and (2) lambda.mountCode is enabled. */ patchTypeScriptPluginMountedCodeLocation() { - if (!this.shouldMountCode() || !this.detectTypescriptPluginType() || !this.isActive()) { + if ( + !this.shouldMountCode() || + !this.detectTypescriptPluginType() || + !this.isActive() + ) { return; } - const template = this.serverless.service.provider.compiledCloudFormationTemplate || {}; + const template = + this.serverless.service.provider.compiledCloudFormationTemplate || {}; const resources = template.Resources || {}; - Object.keys(resources).forEach( - (resName) => { - const resEntry = resources[resName]; - if (resEntry.Type === 'AWS::Lambda::Function') { - resEntry.Properties.Handler = `${this.getTSBuildDir()}/${resEntry.Properties.Handler}`; - } - } - ); + Object.keys(resources).forEach((resName) => { + const resEntry = resources[resName]; + if (resEntry.Type === "AWS::Lambda::Function") { + resEntry.Properties.Handler = `${this.getTSBuildDir()}/${resEntry.Properties.Handler}`; + } + }); } /** @@ -526,13 +581,13 @@ class LocalstackPlugin { patchS3BucketLocationResponse() { const providerRequest = (service, method, params) => { const result = providerRequestOrig(service, method, params); - if (service === 'S3' && method === 'getBucketLocation') { + if (service === "S3" && method === "getBucketLocation") { return result.then((res) => { - if (res.LocationConstraint === 'localhost') { - res.LocationConstraint = 'us-east-1'; + if (res.LocationConstraint === "localhost") { + res.LocationConstraint = "us-east-1"; } return Promise.resolve(res); - }) + }); } return result; }; @@ -545,39 +600,46 @@ class LocalstackPlugin { * Patch S3 createBucket invocation to not add a LocationContraint if the region is `us-east-1` * The default SDK check was against endpoint and not the region directly. */ - patchS3CreateBucketLocationConstraint () { + patchS3CreateBucketLocationConstraint() { AWS.util.update(AWS.S3.prototype, { - createBucket: function createBucket (params, callback) { + createBucket: function createBucket(params, callback) { // When creating a bucket *outside* the classic region, the location // constraint must be set for the bucket and it must match the endpoint. // This chunk of code will set the location constraint param based // on the region (when possible), but it will not override a passed-in // location constraint. - if (typeof params === 'function' || !params) { + if (typeof params === "function" || !params) { callback = callback || params; params = {}; } // copy params so that appending keys does not unintentionallly // mutate params object argument passed in by user var copiedParams = AWS.util.copy(params); - if (this.config.region !== 'us-east-1' && !params.CreateBucketConfiguration) { - copiedParams.CreateBucketConfiguration = { LocationConstraint: this.config.region }; + if ( + this.config.region !== "us-east-1" && + !params.CreateBucketConfiguration + ) { + copiedParams.CreateBucketConfiguration = { + LocationConstraint: this.config.region, + }; } - return this.makeRequest('createBucket', copiedParams, callback); - } - }) + return this.makeRequest("createBucket", copiedParams, callback); + }, + }); } /** * Patch the "serverless-secrets" plugin (if enabled) to use the local SSM service endpoint */ patchServerlessSecrets() { - const slsSecretsAWS = this.findPlugin('ServerlessSecrets'); + const slsSecretsAWS = this.findPlugin("ServerlessSecrets"); if (slsSecretsAWS) { - slsSecretsAWS.config.options.providerOptions = slsSecretsAWS.config.options.providerOptions || {}; - slsSecretsAWS.config.options.providerOptions.endpoint = this.getServiceURL(); - slsSecretsAWS.config.options.providerOptions.accessKeyId = 'test'; - slsSecretsAWS.config.options.providerOptions.secretAccessKey = 'test'; + slsSecretsAWS.config.options.providerOptions = + slsSecretsAWS.config.options.providerOptions || {}; + slsSecretsAWS.config.options.providerOptions.endpoint = + this.getServiceURL(); + slsSecretsAWS.config.options.providerOptions.accessKeyId = "test"; + slsSecretsAWS.config.options.providerOptions.secretAccessKey = "test"; } } @@ -585,12 +647,14 @@ class LocalstackPlugin { * Patch the AWS client library to use our local endpoint URLs. */ async reconfigureAWS() { - if(this.isActive()) { - if(this.reconfiguredEndpoints){ - this.debug("Skipping reconfiguring of endpoints (already reconfigured)") + if (this.isActive()) { + if (this.reconfiguredEndpoints) { + this.debug( + "Skipping reconfiguring of endpoints (already reconfigured)", + ); return; } - this.log('Using serverless-localstack'); + this.log("Using serverless-localstack"); const hostname = await this.getConnectHostname(); const configChanges = {}; @@ -598,10 +662,13 @@ class LocalstackPlugin { // Configure dummy AWS credentials in the environment, to ensure the AWS client libs don't bail. const awsProvider = this.getAwsProvider(); const tmpCreds = awsProvider.getCredentials(); - if (!tmpCreds.credentials){ - const accessKeyId = process.env.AWS_ACCESS_KEY_ID || 'test'; - const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY || 'test'; - const fakeCredentials = new AWS.Credentials({accessKeyId, secretAccessKey}); + if (!tmpCreds.credentials) { + const accessKeyId = process.env.AWS_ACCESS_KEY_ID || "test"; + const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY || "test"; + const fakeCredentials = new AWS.Credentials({ + accessKeyId, + secretAccessKey, + }); configChanges.credentials = fakeCredentials; // set environment variables, ... process.env.AWS_ACCESS_KEY_ID = accessKeyId; @@ -619,7 +686,7 @@ class LocalstackPlugin { this.debug(`Reconfiguring service ${service} to use ${localEndpoint}`); configChanges[serviceLower] = { endpoint: localEndpoint }; - if (serviceLower == 's3') { + if (serviceLower == "s3") { configChanges[serviceLower].s3ForcePathStyle = true; } } @@ -642,14 +709,16 @@ class LocalstackPlugin { // required for compatibility with certain plugin, e.g., serverless-domain-manager awsProvider.cachedCredentials.endpoint = localEndpoint; } - this.log("serverless-localstack: Reconfigured endpoints") + this.log("serverless-localstack: Reconfigured endpoints"); this.reconfiguredEndpoints = true; - } - else { - this.endpoints = {} - this.log("Skipping serverless-localstack:\ncustom.localstack.stages: " + - JSON.stringify(this.config.stages) + "\nstage: " + this.config.stage - ) + } else { + this.endpoints = {}; + this.log( + "Skipping serverless-localstack:\ncustom.localstack.stages: " + + JSON.stringify(this.config.stages) + + "\nstage: " + + this.config.stage, + ); } } @@ -659,22 +728,23 @@ class LocalstackPlugin { loadEndpointsFromDisk(endpointFile) { let endpointJson; - this.debug('Loading endpointJson from ' + endpointFile); + this.debug("Loading endpointJson from " + endpointFile); try { - endpointJson = JSON.parse( fs.readFileSync(endpointFile) ); - } catch(err) { - throw new ReferenceError(`Endpoint file "${this.endpointFile}" is invalid: ${err}`) + endpointJson = JSON.parse(fs.readFileSync(endpointFile)); + } catch (err) { + throw new ReferenceError( + `Endpoint file "${this.endpointFile}" is invalid: ${err}`, + ); } for (const key of Object.keys(endpointJson)) { - this.debug('Intercepting service ' + key); + this.debug("Intercepting service " + key); this.endpoints[key] = endpointJson[key]; } } async interceptRequest(service, method, params) { - // Enable the plugin here, if not yet enabled (the function call below is idempotent). // TODO: It seems that we can potentially remove the hooks / plugin loading logic // entirely and only rely on activating the -> we should evaluate this, as it would @@ -682,17 +752,24 @@ class LocalstackPlugin { this.beforeEventHook(); // Template validation is not supported in LocalStack if (method == "validateTemplate") { - this.log('Skipping template validation: Unsupported in Localstack'); - return Promise.resolve(''); + this.log("Skipping template validation: Unsupported in Localstack"); + return Promise.resolve(""); } - const config = AWS.config[service.toLowerCase()] ? AWS.config : this.getAwsProvider().sdk.config; + const config = AWS.config[service.toLowerCase()] + ? AWS.config + : this.getAwsProvider().sdk.config; if (config[service.toLowerCase()]) { - this.debug(`Using custom endpoint for ${service}: ${config[service.toLowerCase()].endpoint}`); + this.debug( + `Using custom endpoint for ${service}: ${config[service.toLowerCase()].endpoint}`, + ); if (config.s3 && params.TemplateURL) { this.debug(`Overriding S3 templateUrl to ${config.s3.endpoint}`); - params.TemplateURL = params.TemplateURL.replace(/https:\/\/s3.amazonaws.com/, config.s3.endpoint); + params.TemplateURL = params.TemplateURL.replace( + /https:\/\/s3.amazonaws.com/, + config.s3.endpoint, + ); } } await this.reconfigureAWS(); @@ -702,23 +779,25 @@ class LocalstackPlugin { /* Utility functions below */ - getEndpointPort(){ + getEndpointPort() { const url = new URL(awsEndpointUrl); return url.port; } - getEndpointHostname(){ + getEndpointHostname() { const url = new URL(awsEndpointUrl); return url.hostname; } - getEndpointProtocol(){ + getEndpointProtocol() { const url = new URL(awsEndpointUrl); - return url.protocol.replace(":",""); + return url.protocol.replace(":", ""); } getEdgePort() { - return process.env.EDGE_PORT || this.config.edgePort || this.getEndpointPort(); + return ( + process.env.EDGE_PORT || this.config.edgePort || this.getEndpointPort() + ); } /** @@ -730,7 +809,8 @@ class LocalstackPlugin { return resolvedHostname; } - var hostname = process.env.LOCALSTACK_HOSTNAME || this.getEndpointHostname(); + var hostname = + process.env.LOCALSTACK_HOSTNAME || this.getEndpointHostname(); if (this.config.host) { hostname = this.config.host; if (hostname.indexOf("://") !== -1) { @@ -749,8 +829,10 @@ class LocalstackPlugin { const options = { host: hostname, port: port }; await this.checkTCPConnection(options); } catch (e) { - const fallbackHostname = "127.0.0.1" - this.debug(`Reconfiguring hostname to use ${fallbackHostname} (IPv4) because connection to ${hostname} failed`); + const fallbackHostname = "127.0.0.1"; + this.debug( + `Reconfiguring hostname to use ${fallbackHostname} (IPv4) because connection to ${hostname} failed`, + ); hostname = fallbackHostname; } } @@ -776,13 +858,13 @@ class LocalstackPlugin { resolve(); }); - client.setTimeout(500); // milliseconds - client.on("timeout", err => { + client.setTimeout(500); // milliseconds + client.on("timeout", (err) => { client.destroy(); reject(err); }); - client.on("error", err => { + client.on("error", (err) => { client.destroy(); reject(err); }); @@ -790,28 +872,31 @@ class LocalstackPlugin { } getAwsProvider() { - this.awsProvider = this.awsProvider || this.serverless.getProvider('aws'); + this.awsProvider = this.awsProvider || this.serverless.getProvider("aws"); return this.awsProvider; } getServiceURL(hostname) { if (process.env.AWS_ENDPOINT_URL) { - return this.injectHostnameIntoLocalhostURL(process.env.AWS_ENDPOINT_URL, hostname); + return this.injectHostnameIntoLocalhostURL( + process.env.AWS_ENDPOINT_URL, + hostname, + ); } - hostname = hostname || 'localhost'; + hostname = hostname || "localhost"; let proto = this.getEndpointProtocol(); if (process.env.USE_SSL) { - proto = TRUE_VALUES.includes(process.env.USE_SSL) ? 'https' : 'http'; - }else if (this.config.host){ + proto = TRUE_VALUES.includes(process.env.USE_SSL) ? "https" : "http"; + } else if (this.config.host) { proto = this.config.host.split("://")[0]; } const port = this.getEdgePort(); // little hack here - required to remove the default HTTPS port 443, as otherwise // routing for some platforms and ephemeral instances (e.g., on namespace.so) fails const isDefaultPort = - (proto === 'http' && `${port}` === '80') || - (proto === 'https' && `${port}` === '443'); + (proto === "http" && `${port}` === "80") || + (proto === "https" && `${port}` === "443"); if (isDefaultPort) { return `${proto}://${hostname}`; } @@ -824,7 +909,7 @@ class LocalstackPlugin { */ injectHostnameIntoLocalhostURL(endpointURL, hostname) { const url = new URL(endpointURL); - if (hostname && url.hostname === 'localhost') { + if (hostname && url.hostname === "localhost") { url.hostname = hostname; } return url.origin; @@ -843,7 +928,7 @@ class LocalstackPlugin { } sleep(millis) { - return new Promise(resolve => setTimeout(resolve, millis)); + return new Promise((resolve) => setTimeout(resolve, millis)); } clone(obj) { @@ -851,7 +936,7 @@ class LocalstackPlugin { } stepFunctionsReplaceDisplay() { - const plugin = this.findPlugin('ServerlessStepFunctions'); + const plugin = this.findPlugin("ServerlessStepFunctions"); if (plugin) { const endpoint = this.getServiceURL(); plugin.originalDisplay = plugin.display; @@ -859,31 +944,32 @@ class LocalstackPlugin { const newDisplay = function () { const regex = /.*:\/\/([^.]+)\.execute-api[^/]+\/([^/]+)(\/.*)?/g; - let newEndpoint = this.localstackEndpoint +'/restapis/$1/$2/_user_request_$3' - if(this.endpointInfo) { - this.endpointInfo = this.endpointInfo.replace(regex, newEndpoint) + let newEndpoint = + this.localstackEndpoint + "/restapis/$1/$2/_user_request_$3"; + if (this.endpointInfo) { + this.endpointInfo = this.endpointInfo.replace(regex, newEndpoint); } this.originalDisplay(); - } + }; - newDisplay.bind(plugin) + newDisplay.bind(plugin); plugin.display = newDisplay; } } - patchCustomResourceLambdaS3ForcePathStyle () { + patchCustomResourceLambdaS3ForcePathStyle() { const awsProvider = this.awsProvider; const patchMarker = path.join( - awsProvider.serverless.serviceDir, - '.serverless', - '.internal-custom-resources-patched' + awsProvider.serverless.serviceDir, + ".serverless", + ".internal-custom-resources-patched", ); const zipFilePath = path.join( - awsProvider.serverless.serviceDir, - '.serverless', - awsProvider.naming.getCustomResourcesArtifactName() + awsProvider.serverless.serviceDir, + ".serverless", + awsProvider.naming.getCustomResourcesArtifactName(), ); - function fileExists (filePath) { + function fileExists(filePath) { try { const stats = fs.statSync(filePath); return stats.isFile(); @@ -892,40 +978,45 @@ class LocalstackPlugin { } } - function createPatchMarker () { + function createPatchMarker() { try { - fs.open(patchMarker, 'a').close() + fs.open(patchMarker, "a").close(); } catch (err) { return; } } if (fileExists(patchMarker)) { - this.debug("serverless-localstack: Serverless internal CustomResources already patched") + this.debug( + "serverless-localstack: Serverless internal CustomResources already patched", + ); return; } - const customResourceZipExists = fileExists(zipFilePath) + const customResourceZipExists = fileExists(zipFilePath); if (!customResourceZipExists) { return; } const customResources = new AdmZip(zipFilePath); - const utilFile = customResources.getEntry('utils.js') + const utilFile = customResources.getEntry("utils.js"); if (utilFile == null) return; - const data = utilFile.getData().toString() - const patch = "AWS.config.s3ForcePathStyle = true;" + const data = utilFile.getData().toString(); + const patch = "AWS.config.s3ForcePathStyle = true;"; if (data.includes(patch)) { - createPatchMarker() + createPatchMarker(); return; } - const indexPatch = data.indexOf('AWS.config.logger = console;') - const newData = data.slice(0, indexPatch) + patch + '\n' + data.slice(indexPatch) - utilFile.setData(newData) - customResources.writeZip() - createPatchMarker() - this.debug('serverless-localstack: Serverless internal CustomResources patched') + const indexPatch = data.indexOf("AWS.config.logger = console;"); + const newData = + data.slice(0, indexPatch) + patch + "\n" + data.slice(indexPatch); + utilFile.setData(newData); + customResources.writeZip(); + createPatchMarker(); + this.debug( + "serverless-localstack: Serverless internal CustomResources patched", + ); } }