UNPKG

serverless

Version:

Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more

252 lines (207 loc) • 8.68 kB
'use strict'; const _ = require('lodash'); const crypto = require('crypto'); const path = require('path'); const fs = require('fs'); const validate = require('./lib/validate'); const filesize = require('filesize'); class AwsDeployFunction { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.packagePath = this.options.package || this.serverless.service.package.path || path.join(this.serverless.config.servicePath || '.', '.serverless'); this.provider = this.serverless.getProvider('aws'); Object.assign(this, validate); this.hooks = { 'deploy:function:initialize': async () => { await this.validate(); await this.checkIfFunctionExists(); }, 'deploy:function:packageFunction': () => this.serverless.pluginManager.spawn('package:function'), 'deploy:function:deploy': async () => { if (!this.options['update-config']) { await this.deployFunction(); } await this.updateFunctionConfiguration(); await this.serverless.pluginManager.spawn('aws:common:cleanupTempDir'); }, }; } async checkIfFunctionExists() { // check if the function exists in the service this.options.functionObj = this.serverless.service.getFunction(this.options.function); // check if function exists on AWS const params = { FunctionName: this.options.functionObj.name, }; try { const result = await this.provider.request('Lambda', 'getFunction', params); this.serverless.service.provider.remoteFunctionData = result; return result; } catch { const errorMessage = [ `The function "${this.options.function}" you want to update is not yet deployed.`, ' Please run "serverless deploy" to deploy your service.', ' After that you can redeploy your services functions with the', ' "serverless deploy function" command.', ].join(''); throw new this.serverless.classes.Error(errorMessage); } } async normalizeArnRole(role) { if (typeof role === 'string') { if (role.indexOf(':') !== -1) { return role; } const roleResource = this.serverless.service.resources.Resources[role]; if (roleResource.Type !== 'AWS::IAM::Role') { throw new Error('Provided resource is not IAM Role.'); } const roleProperties = roleResource.Properties; if (!roleProperties.RoleName) { throw new this.serverless.classes.Error('Role resource missing RoleName property'); } const compiledFullRoleName = `${roleProperties.Path || '/'}${roleProperties.RoleName}`; const result = await this.provider.getAccountInfo(); return `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}`; } const data = await this.provider.request('IAM', 'getRole', { RoleName: role['Fn::GetAtt'][0], }); return data.Arn; } async callUpdateFunctionConfiguration(params) { await this.provider.request('Lambda', 'updateFunctionConfiguration', params); this.serverless.cli.log(`Successfully updated function: ${this.options.function}`); } async updateFunctionConfiguration() { const functionObj = this.options.functionObj; const serviceObj = this.serverless.service.serviceObject; const providerObj = this.serverless.service.provider; const params = { FunctionName: functionObj.name, }; if ('awsKmsKeyArn' in functionObj && !_.isObject(functionObj.awsKmsKeyArn)) { params.KMSKeyArn = functionObj.awsKmsKeyArn; } else if (serviceObj && 'awsKmsKeyArn' in serviceObj && !_.isObject(serviceObj.awsKmsKeyArn)) { params.KMSKeyArn = serviceObj.awsKmsKeyArn; } if ('description' in functionObj && !_.isObject(functionObj.description)) { params.Description = functionObj.description; } if ('handler' in functionObj && !_.isObject(functionObj.handler)) { params.Handler = functionObj.handler; } if ('memorySize' in functionObj && !_.isObject(functionObj.memorySize)) { params.MemorySize = functionObj.memorySize; } else if ('memorySize' in providerObj && !_.isObject(providerObj.memorySize)) { params.MemorySize = providerObj.memorySize; } if ('timeout' in functionObj && !_.isObject(functionObj.timeout)) { params.Timeout = functionObj.timeout; } else if ('timeout' in providerObj && !_.isObject(providerObj.timeout)) { params.Timeout = providerObj.timeout; } if ( 'layers' in functionObj && Array.isArray(functionObj.layers) && !functionObj.layers.some(_.isObject) ) { params.Layers = functionObj.layers; } if (functionObj.onError && !_.isObject(functionObj.onError)) { params.DeadLetterConfig = { TargetArn: functionObj.onError, }; } if (functionObj.environment || providerObj.environment) { params.Environment = {}; params.Environment.Variables = Object.assign( {}, providerObj.environment, functionObj.environment ); if (Object.values(params.Environment.Variables).some((value) => _.isObject(value))) { delete params.Environment; } else { Object.keys(params.Environment.Variables).forEach((key) => { // taken from the bash man pages if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) { const errorMessage = 'Invalid characters in environment variable'; throw new this.serverless.classes.Error(errorMessage); } if (params.Environment.Variables[key] != null) { params.Environment.Variables[key] = String(params.Environment.Variables[key]); } }); } } if (functionObj.vpc || providerObj.vpc) { const vpc = functionObj.vpc || providerObj.vpc; params.VpcConfig = {}; if (Array.isArray(vpc.securityGroupIds) && !vpc.securityGroupIds.some(_.isObject)) { params.VpcConfig.SecurityGroupIds = vpc.securityGroupIds; } if (Array.isArray(vpc.subnetIds) && !vpc.subnetIds.some(_.isObject)) { params.VpcConfig.SubnetIds = vpc.subnetIds; } if (!Object.keys(params.VpcConfig).length) { delete params.VpcConfig; } } if ('role' in functionObj && !_.isObject(functionObj.role)) { const roleArn = await this.normalizeArnRole(functionObj.role); params.Role = roleArn; await this.callUpdateFunctionConfiguration(params); return; } else if ('role' in providerObj && !_.isObject(providerObj.role)) { const roleArn = await this.normalizeArnRole(providerObj.role); params.Role = roleArn; await this.callUpdateFunctionConfiguration(params); return; } if (!Object.keys(_.omit(params, 'FunctionName')).length) { return; } await this.callUpdateFunctionConfiguration(params); } async deployFunction() { const functionObject = this.serverless.service.getFunction(this.options.function); const params = { FunctionName: this.options.functionObj.name, }; if (functionObject.image) { // Here we are safe to use images that are not fully resolved (with specific SHA) because // AWS always resolves them on their side (as a part of SDK's `updateFunctionCode`) params.ImageUri = functionObject.image; } else { const artifactFileName = this.provider.naming.getFunctionArtifactName(this.options.function); let artifactFilePath = this.serverless.service.package.artifact || path.join(this.packagePath, artifactFileName); // check if an artifact is used in function package level if (_.get(functionObject, 'package.artifact')) { artifactFilePath = functionObject.package.artifact; } const data = fs.readFileSync(artifactFilePath); const remoteHash = this.serverless.service.provider.remoteFunctionData.Configuration .CodeSha256; const localHash = crypto.createHash('sha256').update(data).digest('base64'); if (remoteHash === localHash && !this.options.force) { this.serverless.cli.log('Code not changed. Skipping function deployment.'); return; } params.ZipFile = data; const stats = fs.statSync(artifactFilePath); this.serverless.cli.log( `Uploading function: ${this.options.function} (${filesize(stats.size)})...` ); } await this.provider.request('Lambda', 'updateFunctionCode', params); this.serverless.cli.log(`Successfully deployed function: ${this.options.function}`); } } module.exports = AwsDeployFunction;