Skip to content

Commit

Permalink
fix(AWS Lambda): Ensure function update works when image used (#8786)
Browse files Browse the repository at this point in the history
  • Loading branch information
pgrzesik committed Jan 20, 2021
1 parent aea9f93 commit 420e937
Show file tree
Hide file tree
Showing 2 changed files with 536 additions and 286 deletions.
154 changes: 131 additions & 23 deletions lib/plugins/aws/deployFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ const _ = require('lodash');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
const wait = require('timers-ext/promise/sleep');
const validate = require('./lib/validate');
const filesize = require('filesize');
const ServerlessError = require('../../serverless-error');

class AwsDeployFunction {
constructor(serverless, options) {
Expand All @@ -23,6 +25,7 @@ class AwsDeployFunction {
'deploy:function:initialize': async () => {
await this.validate();
await this.checkIfFunctionExists();
this.checkIfFunctionChangesBetweenImageAndHandler();
},

'deploy:function:packageFunction': () =>
Expand Down Expand Up @@ -63,6 +66,26 @@ class AwsDeployFunction {
}
}

checkIfFunctionChangesBetweenImageAndHandler() {
const functionObject = this.serverless.service.getFunction(this.options.function);
const remoteFunctionPackageType = this.serverless.service.provider.remoteFunctionData
.Configuration.PackageType;

if (functionObject.handler && remoteFunctionPackageType === 'Image') {
throw new ServerlessError(
`The function "${this.options.function}" you want to update with handler was previously packaged as an image. Please run "serverless deploy" to ensure consistent deploy.`,
'DEPLOY_FUNCTION_CHANGE_BETWEEN_HANDLER_AND_IMAGE_ERROR'
);
}

if (functionObject.image && remoteFunctionPackageType === 'Zip') {
throw new ServerlessError(
`The function "${this.options.function}" you want to update with image was previously packaged as zip file. Please run "serverless deploy" to ensure consistent deploy.`,
'DEPLOY_FUNCTION_CHANGE_BETWEEN_HANDLER_AND_IMAGE_ERROR'
);
}
}

async normalizeArnRole(role) {
if (typeof role === 'string') {
if (role.indexOf(':') !== -1) {
Expand Down Expand Up @@ -91,53 +114,105 @@ class AwsDeployFunction {
}

async callUpdateFunctionConfiguration(params) {
await this.provider.request('Lambda', 'updateFunctionConfiguration', params);
const startTime = Date.now();

const callWithRetry = async () => {
try {
await this.provider.request('Lambda', 'updateFunctionConfiguration', params);
} catch (err) {
const didOneMinutePass = Date.now() - startTime > 60 * 1000;

if (err.providerError && err.providerError.code === 'ResourceConflictException') {
if (didOneMinutePass) {
throw new ServerlessError(
'Retry timed out. Please try to deploy your function once again.'
);
}
this.serverless.cli.log(
`Retrying configuration update for function: ${this.options.function}. Reason: ${err.message}`
);
await wait(1000);
await callWithRetry();
} else {
throw err;
}
}
};
await callWithRetry();
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 remoteFunctionConfiguration = this.serverless.service.provider.remoteFunctionData
.Configuration;
const params = {
FunctionName: functionObj.name,
};

if ('awsKmsKeyArn' in functionObj && !_.isObject(functionObj.awsKmsKeyArn)) {
if (functionObj.awsKmsKeyArn && !_.isObject(functionObj.awsKmsKeyArn)) {
params.KMSKeyArn = functionObj.awsKmsKeyArn;
} else if (serviceObj && 'awsKmsKeyArn' in serviceObj && !_.isObject(serviceObj.awsKmsKeyArn)) {
} else if (serviceObj.awsKmsKeyArn && !_.isObject(serviceObj.awsKmsKeyArn)) {
params.KMSKeyArn = serviceObj.awsKmsKeyArn;
}

if ('description' in functionObj && !_.isObject(functionObj.description)) {
if (params.KMSKeyArn && params.KMSKeyArn === remoteFunctionConfiguration.KMSKeyArn) {
delete params.KMSKeyArn;
}

if (
functionObj.description &&
functionObj.description !== remoteFunctionConfiguration.Description
) {
params.Description = functionObj.description;
}

if ('handler' in functionObj && !_.isObject(functionObj.handler)) {
if (functionObj.handler && functionObj.handler !== remoteFunctionConfiguration.Handler) {
params.Handler = functionObj.handler;
}

if ('memorySize' in functionObj && !_.isObject(functionObj.memorySize)) {
if (functionObj.memorySize) {
params.MemorySize = functionObj.memorySize;
} else if ('memorySize' in providerObj && !_.isObject(providerObj.memorySize)) {
} else if (providerObj.memorySize) {
params.MemorySize = providerObj.memorySize;
}

if ('timeout' in functionObj && !_.isObject(functionObj.timeout)) {
if (params.MemorySize && params.MemorySize === remoteFunctionConfiguration.MemorySize) {
delete params.MemorySize;
}

if (functionObj.timeout) {
params.Timeout = functionObj.timeout;
} else if ('timeout' in providerObj && !_.isObject(providerObj.timeout)) {
} else if (providerObj.timeout) {
params.Timeout = providerObj.timeout;
}

if (params.Timeout && params.Timeout === remoteFunctionConfiguration.Timeout) {
delete params.Timeout;
}

if (functionObj.layers && !functionObj.layers.some(_.isObject)) {
params.Layers = functionObj.layers;
}

if (
'layers' in functionObj &&
Array.isArray(functionObj.layers) &&
!functionObj.layers.some(_.isObject)
params.Layers &&
remoteFunctionConfiguration.Layers &&
_.isEqual(
new Set(params.Layers),
new Set(remoteFunctionConfiguration.Layers.map((layer) => layer.Arn))
)
) {
params.Layers = functionObj.layers;
delete params.Layers;
}

if (functionObj.onError && !_.isObject(functionObj.onError)) {
if (
functionObj.onError &&
!_.isObject(functionObj.onError) &&
_.get(remoteFunctionConfiguration, 'DeadLetterConfig.TargetArn', null) !== functionObj.onError
) {
params.DeadLetterConfig = {
TargetArn: functionObj.onError,
};
Expand Down Expand Up @@ -168,6 +243,14 @@ class AwsDeployFunction {
}
}

if (
params.Environment &&
remoteFunctionConfiguration.Environment &&
_.isEqual(params.Environment.Variables, remoteFunctionConfiguration.Environment.Variables)
) {
delete params.Environment;
}

if (functionObj.vpc || providerObj.vpc) {
const vpc = functionObj.vpc || providerObj.vpc;
params.VpcConfig = {};
Expand All @@ -180,25 +263,42 @@ class AwsDeployFunction {
params.VpcConfig.SubnetIds = vpc.subnetIds;
}

if (!Object.keys(params.VpcConfig).length) {
const didVpcChange = () => {
const remoteConfigToCompare = { SecurityGroupIds: [], SubnetIds: [] };
if (remoteFunctionConfiguration.VpcConfig) {
remoteConfigToCompare.SecurityGroupIds =
remoteFunctionConfiguration.VpcConfig.SecurityGroupIds || [];
remoteConfigToCompare.SubnetIds = remoteFunctionConfiguration.VpcConfig.SubnetIds || [];
}
const localConfigToCompare = {
SecurityGroupIds: [],
SubnetIds: [],
...params.VpcConfig,
};
return _.isEqual(remoteConfigToCompare, localConfigToCompare);
};

if (!Object.keys(params.VpcConfig).length || didVpcChange()) {
delete params.VpcConfig;
}
}

if ('role' in functionObj && !_.isObject(functionObj.role)) {
if (functionObj.role && !_.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)) {
} else if (providerObj.role && !_.isObject(providerObj.role)) {
const roleArn = await this.normalizeArnRole(providerObj.role);
params.Role = roleArn;
await this.callUpdateFunctionConfiguration(params);
return;
}

if (params.Role === remoteFunctionConfiguration.Role) {
delete params.Role;
}

if (!Object.keys(_.omit(params, 'FunctionName')).length) {
this.serverless.cli.log(
'Configuration did not change. Skipping function configuration update.'
);
return;
}

Expand All @@ -212,7 +312,15 @@ class AwsDeployFunction {
};

if (functionObject.image) {
const { functionImageUri } = await this.provider.resolveImageUriAndSha(functionObject.image);
const { functionImageUri, functionImageSha } = await this.provider.resolveImageUriAndSha(
functionObject.image
);
const remoteImageSha = this.serverless.service.provider.remoteFunctionData.Configuration
.CodeSha256;
if (remoteImageSha === functionImageSha && !this.options.force) {
this.serverless.cli.log('Image did not change. Skipping function deployment.');
return;
}
params.ImageUri = functionImageUri;
} else {
const artifactFileName = this.provider.naming.getFunctionArtifactName(this.options.function);
Expand Down

0 comments on commit 420e937

Please sign in to comment.